B-Teck!

お仕事からゲームまで幅広く

【GAS】Drive上のファイルの共有リンクを取得し、ダイアログからダウンロードさせる

前回の続きです blog.beatdjam.com

今回はDrive上のファイルの共有リンクを取得し、HTMLで作ったDL用のダイアログを表示させます。
また、スプレッドシートのメニューに任意のメニューを追加する方法も合わせて書きます。

ファイルの共有リンクを取得する

ファイルオブジェクトを取得する

共有リンクを取得するのはFileオブジェクトのIDを知る必要があります。
いくつか方法がありますが、今回はシンプルにDriveAppを用います。
フォルダ名(1階層)・ファイル名を指定して取得する場合はこのように書きます。

/**
 * フォルダ名、ファイル名からFileオブジェクトを取得
 * @param folderName 
 * @param fileName 
 */
function getFileId(folderName, fileName) {
    // フォルダ・ファイルを取得
    const folder = DriveApp.getFoldersByName(folderName).next();
    return folder.getFilesByName(fileName).next();  
}

もしDrive直下のファイルであれば、直接DriveAppから取得できます。

DriveApp.getFilesByName().next();

共有リンクを取得する

Fileには getDownloadUrl() が生えているので、これで共有リンクが取得できます。

function getDownloadUrl(folderName, fileName) {
  // フォルダ・ファイルを取得
  const folder = DriveApp.getFoldersByName(folderName).next();
  return folder.getFilesByName(fileName)
        .next()
        .getDownloadUrl(); // ここでリンク生成  
}

アクセストークンをつける(任意)

共有リンクにはアクセストークンを付与することができます。
組織内で認証が必要な権限のファイルなどをGASで取得するような場合に、アクセストークンがついていないと認証エラーで上手く取得できません。
アクセストークンは ScriptApp.getOAuthToken() で取得することができます。
これを前述したFile.getDownloadUrl()に下記のようにくっつけてやることで、認証可能なURLを生成できます。

file.getDownloadUrl() + "&access_token=" + ScriptApp.getOAuthToken()

HTMLテンプレートを利用してDL用のダイアログを作る

メニューに処理起動メニューを追加する

ここからHTMLをダイアログで表示する機能を作成しますが、その前にスプレッドシートからGASを起動するメニューを作成する必要があります。
というのも、UIに関わる操作はGASのエディタ上からは起動できず、スプレッドシートから起動しないと試せないからです。
手順は簡単で、onOpenハンドラ にメソッドを呼び出す挙動を記述してやるだけです。
今回はダイアログ作成用の関数を呼び出しています。

/**
 * ファイルを開いたときのイベントハンドラで自作メニューを追加
 */
function onOpen() {      
  const menu = SpreadsheetApp.getUi().createMenu('File Download');
  menu.addItem('Exec', 'showDownloadModal');
  menu.addToUi();
}

function showDownloadModal() {
  const url = doCreateZip(); // Zip作成→URL返却
}

この処理を記述した状態でスプレッドシートをリロードすると登録したメニューが現れます。

f:id:beatdjam:20201229173415p:plain

この記事では扱いませんが、より柔軟なメニューの追加はこちらの記事が参考になります。 Google Apps Scriptを使った独自メニューの作り方 - Qiita

これで、ダイアログを開発するための準備が整いました。

Templated htmlについて

GASでは、HTMLファイルのテンプレートを読み込んで、動的なページを生成することができます。
HTML Service: Templated HTML  |  Apps Script  |  Google Developers
ざっくりいうと、下記のような構文が利用できます。

<?= ?> : テキストとして出力される。HTMLタグなどが含まれていた場合はエスケープされる。  
<?!= ?> : テキストとして出力される。HTMLタグが含まれていてもそのまま埋め込まれる。  
<?  ?> : タグ内の処理がスクリプトとして実行される。実行結果は出力されない。  

これを利用するとこんな表示の制御ができます。

  • 要素の表示非表示切り替え
function displaySample() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.isVisible = true;
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Dialog");
}
<html>
  <body>
  <? if (isVisible) {?>
    isVisibleがTrueのときのみこの要素が出力されます。
  <? }?>
  </body>
</html>
  • リンク生成
function displaySample() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.url = "https://example.com/";
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Dialog");
}
<html>
  <body>
        <a href="<?=url?>">Link</a>
  </body>
</html>

ダイアログの表示

先述したリンク生成の内容ほぼそのままですが、これでテンプレートから生成したHTMLをモーダルダイアログとして表示できます。

/**
 * DLを取得してテンプレートからモーダルダイアログを生成して表示する
 */
function showDownloadModal() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.url = doCreateZip(); // Zip作成→URL返却
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Download");
}
<html>
  <body>
    <a href="<?=url?>">Link</a>
  </body>
</html>

f:id:beatdjam:20201229173456p:plain
これで完成です!

おわりに(コード全文)

前回の記事からだいぶ間があいてしまいましたし、GASもリファクタの余地がある状態ですが、ひとまずまとめることができました。
あまりGASを触ったことがないけれど、似たような事がしたい方などに本記事が役立てば良いなと思います。

前回分も含めて記事内で登場したコードの全文を掲載して終わりたいと思います。ありがとうございました!

/**
 * ファイルを開いたときのイベントハンドラで自作メニューを追加
 * 
 */
function onOpen() {      
  const menu = SpreadsheetApp.getUi().createMenu('File Download');
  menu.addItem('Exec', 'showDownloadModal');
  menu.addToUi();
}

/**
 * DLを取得してテンプレートからモーダルダイアログを生成して表示する
 * 
 */
function showDownloadModal() {
  const html = HtmlService.createTemplateFromFile("dialog.html");
  html.url = doCreateZip(); // Zip作成→URL返却
  SpreadsheetApp.getUi().showModalDialog(html.evaluate(), "Download");
}

/**
 * シートからZipファイルを作成してDLリンクを取得
 * 
 */
function doCreateZip() {
    const sheetName = 'sample';
    const jsonKey = 'sample_json';
    const json = JSON.stringify({ jsonKey: getData(sheetName) });

    // 圧縮用のblob作成
    const blobs = new Array();
    blobs.push(Utilities.newBlob(json, 'application/json', 'hoge/fuga.json'));
  
    const fileName = 'archive.zip';
    const zip = Utilities.zip(blobs, fileName);
  
  
    const folderName = 'temporary';
    const folder = getOrCreateTempFolder(folderName);
    const file = createFile(folder, zip);

    return getDownloadUrl("temporary", "archive.zip");
}

/**
 * 与えられたシート名からシートを取得し、表からjsonに変換可能なオブジェクトを生成して返却する
 * 
 * @param sheetName 
 */
function getData(sheetName) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    const rows = sheet.getDataRange().getValues();
    const keys = rows.splice(0, 1)[0];

    return rows.map(row => {
        const obj = {}
        row.forEach((item, index) => { obj[keys[index]] = item; });
        return obj;
    });
}

/**
 * 指定したフォルダ名がすでに存在していればそのフォルダを、
 * 存在しなければ作成したフォルダのオブジェクトを返す
 * 
 * @param folderName 
 */
function getOrCreateTempFolder(folderName) {
    // 未作成なら作成してフォルダを取得する
    const folders = DriveApp.getFoldersByName(folderName);
    if (folders.hasNext()) return folders.next()
    else return DriveApp.createFolder(folderName);
}

/**
 * 与えられたオブジェクトをファイル化して指定したフォルダ内に配置する
 * 
 * @param folder 
 * @param file 
 */
function createFile(folder, file) {
    const files = folder.getFilesByName(file.getName());
    if (files.hasNext()) folder.removeFile(files.next());
    return folder.createFile(file);
}

/**
 * フォルダ名とファイル名からDLリンクを生成します
 * 
 * @param folderName
 * @param fileName
 */
function getDownloadUrl(folderName, fileName) {
  // フォルダ・ファイルを取得
  const folder = DriveApp.getFoldersByName(folderName).next();
  return folder
            .getFilesByName(fileName)
            .next()
            .getDownloadUrl(); // ここでリンク生成  
}