2024年8月24日

GASを使って複数ファイルの一括ダウンロード実証


宮島弥山山頂から
宮島弥山山頂から


久しぶりに宮島の弥山に行ってきました。暑い夏の登山は結構疲れます。

さて,前回はローカルPCファイルからGoogleドライブへの一括アップロードでしたが,今回はGoogleドライブからローカルPCへの一括ダウンロードになります。

ブックマークレットの検証で暫く時間をとられましたが,GASのダウンロードについて,ある程度知見を得ましたので投稿します。

業務の自動化に関するシステムを作成していると,どうしてもGoogleドライブとローカルPC間で多数のファイルのやり取りが発生する場合があります。

Googleドライブからダウンロードする簡単な方法としては,GoogleドライブファイルのダウンロードURLを取得してwindow.open関数を利用する方法が考えられます。

しかし,window.open関数ではポップアップブロック問題があるため,どうしても1ファイル毎に手動によるダウンロード指示が入ります。

これでは,一括してファイルをダウンロードしたい場合に具合が悪く自動化に適しません。

そこで,今回は前回と同様にBase64エンコードを利用したやり方で,複数ファイルを一括してダウンロードしてみたいと思います。

それでは学習を始めましょう。





  今回のGASスクリプトのあらまし


今回作成するスクリプトの外観図は以下のようになります。

GASダウンロード外観図
GASダウンロード外観図


今回はスプレッドシート上に,フォルダID・MIMEタイプ・ファイル名・ファイルID・ダウンロードURL等を表示し,ダウンロードしたいファイルを指定できるようにしました。

スクリプトで全てコード化してもいいのですが,スプレッドシートを利用することで,該当するフォルダを変えるのも簡単ですし,ダウンロードファイルを選択することも可能になります。

なお,今回はスプレッドシートやドキュメントをダウンロードの対象にしていません。これらのファイルは,Blobオブジェクトを取得した時点でPDFに自動的に変換されますので,今回はそのままの状態でダウンロードできるファイルに限定しました。

但し,ドライブに格納されているPDFファイルは対象にしています。


  シーケンスフローについて


今回のシーケンスフローは以下のとおりです。

①起動(GASスクリプト起動)
スプレッドシートにダウンロードしたいファイルが属するフォルダIDとMIMEタイプを入力して,ファイル検索ボタンを押下します。これによりダウンロード対象ファイルをスプレッドシート上に表示するためのGASスクリプトが起動します。

②表示(スプレッドシートを表示する)
指定されたフォルダから,ダウンロード対象ファイル抽出して,スプレッドシート上に表示します。

スプレッドシートのイメージは,以下のとおりです。


スプレッドシートのイメージ図
スプレッドシートのイメージ図



③ダウンロードファイル選択(スプレッドシート)
ダウンロードしたいファイルをチェックします。

④ダウンロード実行(GASスクリプト起動)
ダウンロード実行ボタンを押すことで,該当するGASスクリプトを起動します。これにより,ダウンロードするファイルの存在を確認します。

⑤モーダルダイアログの表示(HTML側)
ダウンロードするファイルが存在すれば,簡易ブラウザであるモーダルダイアログを表示します。

モーダルダイアログのイメージは次のとおりです。

モーダルダイアログ図
モーダルダイアログ図


⑥ダウンロード開始(GASスクリプト起動)
モーダルダイアログのダウンロード開始ボタンを押下することで,「google.script.run」関数 を使ってダウンロードファイル情報を収集するスクリプトを起動します。


⑦ダウンロード情報の取得(HTML側)
「google.script.run」関数 のリターン値からダウンロードファイル情報群を取得します。


⑧ダウンロードの実行(HTML側)と後処理(GAS側)

ダウンロードファイル情報群から,HTML側のa要素を使ってローカルPCに一括ダウンロードします。

また,ダウンロード終了時にgoogle.script.run.関数 を使ってGASスクリプトを呼びだし,スプレッドシート上のダウンロードファイル指定を解除します。

この流れに従って,コードを作成します。



  スプレッドシートを表示するGASスクリプト

まずは,スプレッドシートを表示するGASスクリプト(①②)のコーディングです。このスクリプトはスプレッドシートの「ファイル検索ボタン」に割り付けます。

main.gs

function main() {
  //スプレッドシートから検索するフォルダーIDと検索するMIMETYPEを読み込む
  //Activeシートオブジェクト指定
  var sheet = SpreadsheetApp.getActiveSheet();        //スプレッドシートクラス指定
 
  //スプレッドシートからフォルダーIDを読み込む
  var folder_id = sheet.getRange(2,3).getValue();
  console.log(folder_id);
  
  //エラーメッセージ欄をクリアする。
  sheet.getRange(2,4).setValue(" ");                  
  sheet.getRange(4,4).setValue(" ");

  //フォルダーIDからフォルダーオブジェクトを取得する
  var folder = getfolderobj(folder_id);
  if(folder == "err"){
    sheet.getRange(2,4).setValue("フォルダーIDがありません。再度実行して下さい。");
    console.log("フォルダーIDがありません。");           //エラー表示
    return;
  }
  
  console.log(folder.getName()); //現在のドライブ
  console.log(folder.getId());   //現在のドライブ
  
  //スプレッドシートからMIMEタイプを読み込む
  var mime_type =  sheet.getRange(4,3).getValue();  
  if (mime_type == ""){
    sheet.getRange(4,4).setValue("MIMEタイプがありません。指定して下さい。");
    console.log("MIMEタイプがありません。");   //エラー表示      
    return;    
  }
  //MIMEタイプを配列に分解する
  var mime_types = mime_type.split(/,/g);
  console.log(mime_types.length);
 
  //検索条件文字列を初期化する
  var serch_cond = "";
  //MIME_TYPEから検索条件文字列を編集する。
  for(let i=0; i < mime_types.length; i++){
    switch(mime_types[i]){
      case "text/plain":
      case "text/csv":
      case "text/html":
      case "application/pdf":
      case "image/jpeg":
      case "image/png":
        if ( i == 0 ){
          serch_cond = serch_cond + 'mimeType = "' + mime_types[i] + '"';
        }
        else{
          serch_cond = serch_cond + ' or mimeType = "' + mime_types[i] + '"';
        }
        continue;
      default:
        sheet.getRange(4,4).setValue("指定できないMIMEタイプがあります。ご確認下さい。");
        console.log("使えないMIMEタイプがあります。");   //エラー表示      
        return;
    }
  }
    
  console.log("serch_cond=" + serch_cond);
 
  let row_count = sheet.getLastRow() -7;    //HEADER7行分を除く
  let col_count = sheet.getLastColumn() -1; //利用カラムの最大数

  if(row_count > 0){
    //8行目から最終行までをクリア
    sheet.getRange(8, 1, row_count, col_count+1).clear({contentsOnly: true, skipFilteredRows: true});
  }
 
  let row = 8;
  let counter = 1;

  //該当フォルダー下の該当するファイルのコレクションを取得する
  const files = folder.searchFiles(serch_cond);
  //console.log(files);
  while (files.hasNext()){              //コレクションファイルが存在する間繰り返す
    var file =files.next();             //コレクションファイルを読む
    console.log(file.getName());        //ファイル名を出力する
      
    //ファイル名,ファイルID,ファイルURLを編集する
    let col = 2;
    while (col < 7){                    //配列の数だけ繰り返す
      sheet.getRange(row,col).setBorder(true,true,true,true,true,true); //セル上下左右罫線書く
      sheet.getRange(row,col).setVerticalAlignment('middle');   //セル垂直中央に設定
      switch(col){
        case 2:  //項番を編集する
          sheet.getRange(row,col).setHorizontalAlignment('center'); //セル水平中央に設定
          sheet.getRange(row,col).setValue(counter);
          break;
        case 3:  //ファイル名を編集する
          sheet.getRange(row,col).setValue(file.getName());
          break;
        case 4:  //ファイルIDを編集する
          sheet.getRange(row,col).setWrap(true);             //セル内折り返し設定
          sheet.getRange(row,col).setValue(file.getId());    //ファイルIDを編集する
          break;
        case 5:  //ファイルURLを編集する
          sheet.getRange(row,col).setWrap(true);             //セル内折り返し設定
          sheet.getRange(row,col).setValue(file.getDownloadUrl()); //ファイルDWLURLを編集する
          break;
        case 6:
          sheet.getRange(row,col).setHorizontalAlignment('center'); //セル水平中央に設定
          sheet.getRange(row,col).insertCheckboxes();
      }
      col++;
    }
    row++;
    counter++;
  }
  SpreadsheetApp.flush();
  return;
}

function getfolderobj(folder_id) {
  try {
    return DriveApp.getFolderById(folder_id);
  }
  catch(e) {
    return "err";
  }
}
【補足】

このスクリプトは,単に与えらたフォルダーID下にあるファイルを,MIMEタイプに従って検索し,スプレッドシートに表示しています。

表示項目としては,ファイル毎に項番,ファイル名,ファイルID,ダウンロードURL,チェックボックスとなっています。ファイルダウンロードURLなどは必要ないのですが,学習のために取得表示しています。

所々に「console.log」というデバッグ用の記述がありますが,無視してください。



  ダウンロードファイルの存在確認とモーダルダイアログの表示

次に,スプレッドシートのダウンロード実行ボタンを押下することでダウンロードファイルの存在確認とモーダルダイアログを表示します。

GASスクリプトとHTMLスクリプト(④⑤)のコードは以下のとおりです。


dwl_make.gs

function dwl_perform(){
  // ダウンロードファイルの存在を確認する
  var files = dwl_exist();
  
  if(files[0][0] == ""){
    Browser.msgBox("ダウンロードファイルが指定されていません");
    return;
  }
  
  //htmlを起動する。
  var output = HtmlService.createHtmlOutputFromFile("index");
  SpreadsheetApp.getUi().showModalDialog(output, 'ダウンロードファイル指示');
}

function dwl_exist(){
  //データ配列の初期化
  var files = [["","","","",""]];
  
  //Activeシートオブジェクト指定
  var sheet = SpreadsheetApp.getActiveSheet(); //スプレッドシートクラス指定
  
  //ダウンロードファイルが指示されているか,確認する。
  let row_count = sheet.getLastRow() -7;    //HEADER7行分を除く
  let col_count = sheet.getLastColumn() -1; //利用カラムの最大数

  if(row_count < 1){
    Browser.msgBox("ダウンロードすべきファイルがありません,再度検索実行後指示して下さい");
    return;
  }
  //ダウンロード指示されたファイルを探す
  let i = 0;
  var dwl_filename = "";
  var dwl_fileid   = "";
  var dwl_fileurl  = "";
  let row = 8;
  //ダウンロードファイル名,ダウンロードURL,zダウンロードIDを取得する
  while(row < (row_count + 8)){ 
    if(sheet.getRange(row, 6).getValue() == true){     //ダウンロード指示有を確認
      dwl_filename = sheet.getRange(row,3).getValue(); //ファイル名を退避
      dwl_fileid = sheet.getRange(row,4).getValue();   //ファイルIDを退避
      dwl_fileurl = sheet.getRange(row,5).getValue();  //ファイルURLを退避      
      files[i] = [dwl_filename, dwl_fileid, dwl_fileurl, row, ""];
      file_obj = DriveApp.getFileById(dwl_fileid);
      blob_obj = file_obj.getBlob();
      files[i][4] = `data:${blob_obj.getContentType()};base64,${Utilities.base64Encode(blob_obj.getBytes())}`; 
      console.log(files[i][4]);
      i = i + 1;
    }  
    row = row + 1;
  }
  return files;
}

【補足】

このスクリプトは,ダウンロードファイルの存在を確認し,存在すればモーダルダイアログを表示しています。

dwl_exist関数は,スプレッドシート内を検索してダウンロード指示されたファイルの情報を「files」という2次元配列に格納しています。

この中で最も注意すべきは,「files」の各行毎(ファイル毎)にファイルの中身をBase64エンコードしたデータURL形式データとして配列内に格納している点です。

データ形式は以下のようになります。

data:${blob_obj.getContentType()};base64,${Utilities.base64Encode(blob_obj.getBytes())}

この式では,$マークを使って変数を文字列として扱っていますので,「`」(バッククォート)で囲むことに注意してください。

このスクリプトをスプレッドシートの「ダウンロード実行ボタン」に割り付けます。



次に,モーダルダイアログを表示するコードを作ります。

index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
     #drop {
      height: 50px;
      border: #D4D4D4;
      border-style: dashed;
      border-width: 3px;
      padding: 10px;
      text-align: center;
      } 
     #click {
      text-align: right;
     }
     #result {
      text-align: center;
     }    
    </style>
  </head>
  <body>
    <div>
      <div id="drop">
         <p>チェックしたファイルをダウンロードします
          <button id="Dwl_button" type="button">ダウンロード開始</button>
        </p>
      </div>
      <hr />
      <div id="click">
        <button id="click_1" type="button" >実行</button>
        <button id="click_2" type="button" >終了</button>
      </div>
      <div>
        <pre id="result"></pre>
      </div>
    </div>
  </body>
</html>

【補足】

モーダルダイアログのHTMLについては,イベントドリブン型でjavascriptが記述できるようにしています。




  ダウンロードファイル情報の取得

次に,モーダルダイアログの「ダウンロード開始ボタン」を押下することで,ダウンロードファイル情報を取得するjavascriptとGASスクリプト(⑥⑦)コードを作ります。

GASとの連携には,「google.script.run」関数を使って任意のGAS関数を起動します。

なお,このスクリプトには日付を組み立てる関数「getToday()」とスクリプトを終了する関数「spread_syuryo()」を加えています。

このjavascriptをindex.htmlの</html>の前に挿入します。

index.html

<script>
  //退避エリアを初期値する
  var dwl_files = [["","","","",""]];
  window.addEventListener('DOMContentLoaded', function() {
    document.getElementById("Dwl_button").addEventListener('click', function(e){
      //引数エリアを初期化する。
      var files = [["","","","",""]];      
      //ダウンロードファイルの情報を取得する
      google.script.run
      .withSuccessHandler(function(files){
        
        dwl_files = files; //引数エリアを退避する。
        //alert("files= " + files[0][0]);
           
        if(files[0][0] === ""){
          var msg = "日付=" + getToday() + "  ダウンロードファイルはありません。";
          msg = msg + "\n\n" + "終了ボタンを押して下さい。";
        } else {
          var msg = "日付=" + getToday() + "  ファイル名=" + files[0][0]; 
          msg = msg + "\n\n" + "ダウンロードします。実行ボタンを押して下さい"; 
        }
        document.getElementById('result').textContent = msg;
      })
      .withFailureHandler(function(err) {  //ファイル情報取得に失敗した場合の処理
        alert("ダウンロードが失敗しました。\n\nエラーメッセージ=" + err.message);
      }).dwl_exist(files);
    },true);

    //*** 次の関数をココに入れる。 ***
  });
  
  function getToday(){
    let today = new Date();
    today.setDate(today.getDate());
    const yyyy = today.getFullYear();
    const mm = ("0"+(today.getMonth()+1)).slice(-2);
    const dd = ("0"+today.getDate()).slice(-2);
    const result = yyyy+'-'+mm+'-'+dd;
    return result;
  }

  function spread_syuryo(){
    // スプレッドシートで、アップロードUIを自動的に閉じる
    google.script.host.close();
  }
</script>

【補足】

このスクリプトは,イベントドリブン型のスクリプト関数になっています。「window.addEventListener('DOMContentLoaded', function()」でページが開くのを待って,「ダウンロード開始ボタン」がクリックされると起動します。

「google.script.run」関数を使って,GAS側の「dwl_exist()」関数を呼びだして,ダウンロードファイル情報を「files」という2次元配列で受け取っています。

ダウンロードするファイルがあれば,その1番目をモーダルダイアログに表示して,実行ボタンを押すように促します。

「dwl_exist()」関数の起動に失敗した場合は,エラーメッセージを出力します。



  ダウンロードの実行とスプレッドシートの後処理

ダウンロードの実行(HTML側)(⑧)とスプレッドシートの後処理(GAS側)のjavascriptとGASスクリプトのコードを作ります。

モーダルダイアログの「実行ボタン」を押すことで動作します。また,「終了ボタン」を押すとスクリプト終了関数が動作します。

以下に,HTML側のjavascriptのコードを記載します。このコードは,前述の「document.getElementById("Dwl_button").addEventListener('click', function(e)」の次の関数として設定します。


index.html

    document.getElementById("click_1").addEventListener('click', async function(e){
      // 退避エリアから引数を戻す
      files = dwl_files;
      
      if(files[0][0] === ""){
        alert("ダウンロードファイルがありません。ダウンロード開始ボタンを押して下さい。")
        return;
      }
      const sleep = ms => new Promise(resolve => setTimeout(resolve, ms || 1000));
      for( let i = 0; i < files.length; i++){
        var msg = "日付=" + getToday() + "  ファイル名=" + files[i][0]; 
        msg = msg + "\n\n" + "ダウンロードします。実行ボタンを押して下さい"; 
        document.getElementById('result').textContent = msg;
        let a = document.createElement("a");
        document.body.appendChild(a);
        a.download = files[i][0];
        a.href = files[i][4];
        a.click();
        var ms = 3000;
        //alert("時間待ち" + ms.toString());
        await sleep(ms);
        //alert("秒経過=" + ms.toString());
      }
      google.script.run
      .withSuccessHandler(function(files){
          
        dwl_files = files; //引数エリアを退避する。
          
        var msg = "日付=" + getToday() + "  ダウンロードは終了しました。";
        msg = msg + "\n\n" + "終了ボタンを押して下さい。";
        document.getElementById('result').textContent = msg;
      })
      .withFailureHandler(function(err) {  //ファイル情報取得に失敗した場合の処理
        alert("ダウンロード終了処理が失敗しました。\n\nエラーメッセージ=" + err.message);
      }).dwlfile_syuryo(files);
    },true);

    document.getElementById("click_2").addEventListener('click', function(e){
      //alert("終了ボタンが押されました");
      spread_syuryo() 
    },true);

【補足】

「document.getElementById("click_1").addEventListener('click', async function(e)」の関数は,「await関数」を使っているため,async宣言をしていることにご注意ください。

また,GAS側より受け取った二次元配列「files」を利用して,body要素にa要素を追加し,
downloadを繰り返しています。

download先のローカルPCフォルダは,ブラウザのダウンロード設定項目で指定できます。ブラウザからダウンロード許可を求められた場合は,許可してください。

a要素のhref属性に,Base64エンコードで作成したデータURLを設定しています。なお,download間隔をある程度保つため,3秒間隔で処理しています。

sleep関数は,「const sleep = ms => new Promise(resolve => setTimeout(resolve, ms || 1000));」で定義しています。

なお,終了ボタンが押された場合は,「document.getElementById("click_2").addEventListener('click', function(e)」が動作します。



次に,「google.script.run」関数で呼ばれるGASの関数「dwlfile_syuryo(files)」のコードを以下に示します。


dwl_make.gs

function dwlfile_syuryo(files){
  
  //Activeシートオブジェクト指定
  var sheet = SpreadsheetApp.getActiveSheet(); //スプレッドシートクラス指定
  
  console.log("ファイル名 = " + files[0][0]);
  console.log("ファイルID = " + files[0][1]);
  console.log("ファイルURL= " + files[0][2]);
  console.log("row= " + files[0][3]);
  console.log("ファイルDate =" + files[0][4]);

  //ダウンロード処理済みのファイルがある時は,チェックを消す。
  for(let i = 0; i < files.length; i++){
    if(files != undefined && files[i][0] != ""){
      sheet.getRange(files[i][3],6,1,1).clear({contentsOnly: true, skipFilteredRows: true});
      sheet.getRange(files[i][3],7,1,1).clear({contentsOnly: true, skipFilteredRows: true});
      sheet.getRange(files[i][3],6,1,1).setHorizontalAlignment('center'); //セル水平中央に設定
      sheet.getRange(files[i][3],6,1,1).insertCheckboxes();
    }
  }
  files = [["","","","",""]];
  return files;
}

【補足】

この関数では,スプレッドシート上のダウンロードファイル指定をクリアしています。


  まとめ

今回は,前回のアップロードの反対に複数ファイルを一括でダウンロードする方法を学習しました。

a要素を使ってダウンロードし,実用的であるとは思います。

なお,ダウンロードURLが取得出来るので,HTTPリクエストのUrlfetchAppクラスでも可能ではないかと思いますが,今後の研究課題とします。

今回は,時間調整としてsetTimeout関数を利用しています。Promiseを使った時間待ちでは,async宣言関数の中だけ,時間待ちすることができるようです。この点は,注意して使う必要があると感じました。

まだまだ,わからないことが多いですが,一応,ローカルPCとGoogleドライブ間連携の目途が立ちました。

少し,長くなりましたが,以上です。

それでは,楽しいITリテラシーライフをお過ごしください。


(ご注意)情報の正確性を期していますが,実施される場合には自己責任でお願いします。


【追伸】

最後に,ダウンロードURLを使って,UrlfetchApp.fetch()関数からGoogleドライブ内のファイルを読み取ろうと試みましたが,ログイン画面がリターンされ読み取ることができませんした。

過去情報では,ScriptApp.getOAuthToken()でTokenを得て,UrlfetchApp.fetch()のオプションに指定すれば出来るようでしたが,効果ありませんでした。

知識が足りないのかもしれませんが,調査に時間がかかりますので断念することにしました。UrlfetchApp.fetch()を利用しなくても,DriveApp.getFileById(”ファイルID”).getBlob()でファイルを直接取得することができますので,特に問題ないと考えます。


0 件のコメント: