Google Apps Scriptでファイルアップローダを作る【GAS】

以前、GoogleのPicker APIを利用したアップローダを作成しました。正直な所、このAPIは非常に強力で、Google Driveと連携するアップローダやファイル選択ダイアログならばこれを素直に使ったほうがベストです。しかし、一方で他のサービスなどには利用出来ないので、Google Drive以外で使うシーンでは、これまで通り、HTMLでのアップロードの仕組みを構築する必要があります。

今回、単体のアップロード、File APIを利用した複数のファイルのアップロードの2種類を作成してみました。尚、複数ファイルの選択は、CtrlキーやShiftキーを押しながら選択する事が出来ます。

※2020/02/20現在、未だにV8でformとinputを使ってのアップロードだとファイルが壊れますので、Chrome V8ランタイムオフにしておくのがベストです。

今回使用するスプレッドシート等

Google Pickerを使った手法のほうが見た目も扱いも楽なので、個人的にはそちらを推奨します。その内容は以下のエントリーにまとめてあります。

Google Picker アップローダを作る【GAS】

form-inputを使った手法

単体アップローダの場合

GAS側コード

var folderId = "ここにアップロードする先のfolderのIDを入れる";
 
function uploadman() {
  var output = HtmlService.createTemplateFromFile('Form');
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var html = output.evaluate();
  ss.show(html);
}

function sendForm(theForm) {
  var fileBlob = theForm.myFile;
  var folder = DriveApp.getFolderById(folderId);
  var doc = folder.createFile(fileBlob);
  
  Browser.msgBox("終了したよ");
}
  • uploadmanがダイアログ表示を行うスクリプトです。
  • sendFormはHTML側から叩いて実行する実際にアップロード作業を行うスクリプトです。
  • theFormという引数部分で、ファイルをBLOB形式で受け取ります。myFileとはHTML側のアップロードするボタンに付けられてるname属性値です。

HTML側コード

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
 
<div id="formman">
<form id="myForm">
<input name="myFile" type="file" multiple />
   <br><br><br><br>
<input type="button" class="action" value="アップロード" onclick="google.script.run.sendForm(this.parentNode)" />
</form>
</div>
  • 非常にシンプルです。アップロードボタンには、google.script.run.sendFormというGAS側の関数を叩くコマンドを入れてあります。
  • this.parentNodeでformの中身をまとめて引数として送りつけています。

実行結果

スプレッドシートを開き、上部にある「▶あぷろだ」メニューの中の【ファイル選択】を開くと、アップロード用のダイアログが開きます。但し、実際にはファイルを作るメソッドはオフにしてありますので、実際にはサンプルからはアップロードはできません。コピーして該当箇所のコメントアウトを解除し、folderIdを入れて使いましょう。

図:ダイアログ上からファイルをアップロードする

ポイント

  1. ダイアログは必ず、createTemplateFromFileメソッドで作成して出力しなければなりません。
  2. 仕様上、ファイルは1個ずつしか選択できません。但し、HTML5が使えるので、multipleを指定すると複数選択出来るようになります。サンプルスプレッドシートは複数選択出来るようにしています。複数選択は、Ctrlキーを押しながらファイルを選択するだけです。多分配列で渡されるんじゃなかろうか。
  3. G Suitesなどで使う場合、当たり前ですがフォルダに対してのアクセス権がない場合、アップロードが出来ません。但し、実行権限を各人ではなくファイルオーナーにしておくと、アクセス権限がなくともアップロードが可能です。
  4. 所定のフォルダは直接スクリプト内に書くのではなく、Propertie.Serviceなどを使って、スクリプトプロパティなどにフォルダIDを格納し、スクリプト内から呼び出す方式にしたほうが便利です。
  5. アップロード中に例えばスピナー(くるくる回転するgif画像など)や、アップロードしてるよというメッセージ、また、何度もボタンを押せないように送信したら、ボタンを押せなくするなどの対策をしておく必要性があります。

複数ファイルアップローダの場合

GAS側コード

var folderId = "ここにアップロードする先のfolderのIDを入れる";
 
function saveFile(data,name) {
  var contentType = data.substring(5,data.indexOf(';'));
  var file = Utilities.newBlob(Utilities.base64Decode(data.substr(data.indexOf('base64,')+7)), contentType, name);
  DriveApp.getFolderById(folderId).createFile(file);
}
 
//アップローダ表示用
function uploadman() {
  var html = HtmlService.createHtmlOutputFromFile('index')
    .setSandboxMode(HtmlService.SandboxMode.IFRAME)
    .setWidth(400)
    .setHeight(200);
SpreadsheetApp.getUi()
    .showModalDialog(html, '複数あぷろだテスト');
}
 
//完了メッセージを出す
function endmsg(tempmsg){
  var ui = SpreadsheetApp.getUi();
  ui.alert(tempmsg)
}
  • uploadmanがダイアログ表示を行うスクリプトです。
  • saveFileが実際にドライブにファイルを生成するスクリプトです。Blobデータとファイル名を受け取ります。

HTML側コード

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script>
  var reader = new FileReader();
  var files;
  var fileCounter = 0;
  
  //GAS側のファイル保存関数をファイルの個数分回して呼び出すルーチン
  reader.onloadend = function () {
  google.script.run
   .withSuccessHandler(function(){
     fileCounter++;      
     postNextFile();
   }).saveFile(reader.result,files[fileCounter].name);
  }
 
  //受信したファイルをドライブに保存するルーチン
  function SaveFiles(){
    files = document.getElementById("myFiles").files;  
    postNextFile();
  }
   
  //実際にreadAsDataURLをもってreaderにファイルを格納するルーチン
  function postNextFile(){
    if(fileCounter < files.length){
      reader.readAsDataURL(files[fileCounter]);
    }else{
      fileCounter=0;
      google.script.run.endmsg("終了しました。");
    }
  }
</script>
<div>
  <form method="post" enctype="multipart/form-data">
  <input type="file"  id="myFiles" name="myFiles" multiple/>
  <input type="button" value="Submit" class="action" onclick=" SaveFiles()" />
  </form>
</div>

実行結果

スプレッドシートを開き、上部にある「▶複数あぷろだ」メニューの中の【ファイル選択】を開くと、アップロード用のダイアログが開きます。但し、実際にはファイルを作るメソッドはオフにしてありますので、実際にはサンプルからはアップロードはできません。コピーして該当箇所のコメントアウトを解除し、folderIdを入れて使いましょう。

図:複数のファイルをいっぺんにアップロードできる

ポイント

  • 単一のファイルのアップロードと異なり、HTML側でのFile APIに関する処理が加わっています。
  • GAS側のファイルを作る関数を呼び出すルーチンでは、少々特殊な書き方で、ループ処理を行わせています。
  • もちろん、inputには、multipleを入れて置きます。
  • formタグ関係はなくても問題ありません。
  • そのままHTML側からGAS側にデータを渡すと、Base64でエンコードされているので、デコードしてあげないと読めないファイルが出来てしまいます。

上書きアップロード

Google Apps Scriptは他のクラウドストレージやファイルサーバと異なり、同じファイル名が同じフォルダ内に存在する事が可能になっています。ファイルのIDで管理しているためこのような事が実現できるのですが、

ファイルのIDがわかってるものであれば、setContentで上書きが可能なのですが(テキストのみ)、ファイルアップローダのようにアップ先に同じファイル名のものがあるかどうかわからない場合には、このままだとファイルIDを調べるのに一工夫必要になります。また、Drive APIを利用してもアップロードが可能です。その場合は以下の手間が生じます。

  1. スクリプトエディタのメニューより、「リソース」⇒「Googleの拡張サービス」を開く
  2. Drive APIのスイッチをONにする

Drive APIを利用してファイルを上書きアップロードしてみましょう。

//上書きでファイルをアップロードする
function sendForm2(theForm){
  var ui = SpreadsheetApp.getUi();

  //ファイルを受け取る
  var fileBlob = theForm.myFile;
  
  //ファイル名を取り出す
  var filename = fileBlob.name;

  //指定のフォルダを取得する
  var folder = DriveApp.getFolderById(folderId);
  
  //ファイル名を指定してファイルを取得してみる
  var files = folder.getFilesByName(filename);
  
  if (files.hasNext()) {
    //Driveのファイルを上書きアップロード
    Drive.Files.update({}, files.next().getId(), fileBlob);
    
    ui.alert("上書きしたよ");
  } else {
    //ファイルを新規アップロードする
    var doc = folder.createFile(fileBlob);
    ui.alert("新規アップロードしたよ");
  }
}
  • ためしに取得したfilenameで取得してみて、trueならば上書き、falseならば新規アップロードになります。
  • setContentにてファイルを上書きすることが可能ですが、引数はBlobじゃありませんのでBlob形式だとファイルが壊れます。
  • Drive APIを使って、Drive.Files.update({}, files.next().getId(), fileBlob);でも上書きが可能です。

注意点

本項目のアップロード手法は、Chrome V8ランタイムが有効な場合、アップロード後のファイルが破損します(GASのバグですね)。未だにこのバグはIssue Trackerには挙げられていて修正されていません。(このトラブルはtextなどのアップロードは問題なし。問題が起きるのは、画像や動画ファイルなどのバイナリファイル系)

よって、このソースコードで運用する場合には、以下の手順でV8ランタイムをオフにする必要性があります。(新IDEをベースに説明しています)

  1. スクリプトエディタを起動する
  2. 左サイドバーのプロジェクトの設定をクリック
  3. Chrome V8 ランタイムを有効にするチェックを外す

これでOKです。

図:V8が有効だと壊れます

V8対応ライブラリを使った手法

単体アップローダの場合

前項の手法は一般的なウェブで使われてるアップロード手段なのですが、V8ランタイムが有効な場合、アップロード後のファイルが壊れるというバグがあり、GAS上ではV8ランタイムをオフにしなければ利用できません。しかし、そうなるとV8での構文が使えなくなってしまうため、不利益でもあります。

この問題を解決する為のライブラリが公開されており、基本的にはライブラリをロードして、onClickの送信時メソッドにちょっとした加工をするだけで、壊れずにV8ランタイムをオンの状態でアップロードが可能です。

GAS側コード

var folderid = "ここにアップロード先フォルダのIDを指定"

function uploadman2(){
  var output =   HtmlService.createHtmlOutputFromFile('Form2')
            .setSandboxMode(HtmlService.SandboxMode.IFRAME);

  var ss = SpreadsheetApp.getActiveSpreadsheet();
  ss.show(output);
}

//V8対応アップロード
function sendForm3(e) {
  //指定のフォルダを取得する
  var folder = DriveApp.getFolderById(folderId);

  folder.createFile(
    Utilities.newBlob(
      e.file[0].files[0].bytes,
      e.file[0].files[0].mimeType,
      e.file[0].files[0].filename
    )
  );

  SpreadsheetApp.getUi().alert("アップロード完了");
}
  • createFileにてUtilities.newBlobで受取ます。この時、mimetypeやfilenameも指定されています。

HTML側コード

<head>
  <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">

  <!-- V8対応ライブラリ -->
  <script src="https://cdn.jsdelivr.net/gh/tanaikech/HtmlFormObjectParserForGoogleAppsScript_js@master/htmlFormObjectParserForGoogleAppsScript_js.min.js"></script>
</head>

<body>
  <div id="formman">
    <form>
      <input type="file" name="file" />
      <input
        type="button"
        value="ok"
        class="action"
        onclick="ParseFormObjectForGAS(this.parentNode).then(obj => google.script.run.sendForm3(obj))"
      />
    </form>
  </div>
</body>
  • V8対応ライブラリをロードしておきます
  • formのinputのonclickではParseFormObjectForGASでアップロードファイルを受取り、google.script.runにてGAS側へ渡すようにコードを記述します。

実行結果

本コードはV8ランタイムがオフである場合、動作しません。スプレッドシートを開き、上部にある「▶ファイル選択-V8対応」を開くと、アップロード用のダイアログが開きます。但し、フォルダIDは空っぽなので、実際にはサンプルからはアップロードはできません。folderIdを入れて使いましょう。

50MBオーバー対応アップローダ

通常のgoogle.script.run.withSuccessHandlerを使った手法の場合、Google Apps Scriptの制限によりファイルサイズは最大50MBまでしかアップロードすることが出来ません。しかし、Drive API V3を使った手法の場合、レジュームアップロードを利用することで、50MBの制限を超えてアップロードする事が可能です。

50MBオーバー対応の為のライブラリが公開されており、またgoogle.script.runを利用していないので、V8でも問題なく動作します。

GAS側コード

var folderid="ここにアップロード先フォルダIDを指定"

//V8対応レジュームアップロード対応用
function uploadman2() {
  var html = HtmlService.createHtmlOutputFromFile('Form2')
    .setSandboxMode(HtmlService.SandboxMode.IFRAME)
    .setWidth(400)
    .setHeight(200);
  SpreadsheetApp.getUi().showModalDialog(html, '複数あぷろだテスト V8対応版');
}

//Access Tokenを取得する
function getAuth() {
  // DriveApp.createFile(blob);
  return ScriptApp.getOAuthToken();
}

//アップロード時のロックを管理し、スプレッドシートに記録する
function putFileInf(obj) {
  var lock = LockService.getDocumentLock();
  if (lock.tryLock(5000)) {
    SpreadsheetApp.getActiveSpreadsheet()
      .getSheets()[0]
      .appendRow([obj.name, obj.mimeType, obj.id]);
    lock.releaseLock();
  }
}

//フォルダのIDをForm2へと送り込む
function getfolderid(){
  return folderId;
}
  • 複数ファイルを同時にアップロードするために、putFileInfにてロック制御やアップロードしたファイルの情報をスプレッドシートに記録しています。
  • getfolderidにてGAS側に記述しているアップロード先フォルダIDをHTML側へ送っています。

HTML側コード

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
    <script src="https://cdn.jsdelivr.net/gh/tanaikech/ResumableUploadForGoogleDrive_js@master/resumableupload_js.min.js"></script>
    <script>
      
      //アップロード先フォルダID
      var folderid;

      //フォルダのIDをGAS側から取得する
      google.script.run.withSuccessHandler(onSuccess).getfolderid();

      function onSuccess(data){
        folderid = data;
      }

      //Access Tokenを取得する
      function run() {
        google.script.run
          .withSuccessHandler(accessToken =>
            ResumableUploadForGoogleDrive(accessToken)
          )
          .getAuth();
      }

      //50MB以上のファイルを複数レジュームアップロードする
      function ResumableUploadForGoogleDrive(accessToken) {
        const f = document.getElementById("file");
        [...f.files].forEach((file, i) => {
          let fr = new FileReader();
          fr.fileName = file.name;
          fr.fileSize = file.size;
          fr.fileType = file.type;
          fr.readAsArrayBuffer(file);
          fr.onload = e => {
            var id = "p" + ++i;
            var div = document.createElement("div");
            div.id = id;
            document.getElementById("progress").appendChild(div);
            document.getElementById(id).innerHTML = "Initializing.";
            const f = e.target;

            //アップロードするファイルの情報を指定
            const resource = {
              fileName: f.fileName,
              fileSize: f.fileSize,
              fileType: f.fileType,
              fileBuffer: f.result,
              accessToken: accessToken,
              folderId:folderid  //ここでアップロード先フォルダのIDを指定
            };
            const ru = new ResumableUploadToGoogleDrive();
            ru.Do(resource, function(res, err) {
              if (err) {
                console.log(err);
                return;
              }
              console.log(res);
              let msg = "";
              if (res.status == "Uploading") {
                msg =
                  Math.round(
                    (res.progressNumber.current / res.progressNumber.end) * 100
                  ) +
                  "% (" +
                  f.fileName +
                  ")";
              } else {
                msg = res.status + " (" + f.fileName + ")";
              }

              // If you want to put the uploaded file information to the active Spreadsheet,
              // please use the following function.
              if (res.status == "Done") google.script.run.putFileInf(res.result);

              document.getElementById(id).innerText = msg;
            });
          };
        });
      }
    </script>

  </head>
  <body>
    <input type="file" id="file" multiple="true" />
    <input type="button" class="action" onclick="run()" value="Upload" />
    <div id="progress"></div>
  </body>
</html>
  • 50MBオーバー対応ライブラリをロードしておきます。
  • HTML表示時にGAS側からアップロード先フォルダのIDを取得しています。
  • upload実行時にrun()が実行され、Access Tokenが取得されます。
  • Access Tokenをもって、Drive API V3をもって、HTML側から直接Google Driveにファイルがアップロードされます。
  • よって、今回はアップロードの為に、google.script.runは利用していません

実行結果

本コードはV8ランタイムがオフである場合、動作しません。スプレッドシートを開き、上部にある「▶複数あぷろだ」→「ファイル選択-V8対応版」を開くと、アップロード用のダイアログが開きます。但し、フォルダIDは空っぽなので、実際にはサンプルからはアップロードはできません。folderIdを入れて使いましょう。

図:複数同時にアップも可能です。

関連リンク

Google Apps Scriptでファイルアップローダを作る【GAS】” に対して4件のコメントがあります。

  1. 出川和人 より:

    サンプルコードと丁寧な解説をありがとうございます!
    GASのメモリ上に一時ファイルとして配置されたバイナリファイルをGドラで読み込める形に変換して取得するという理解で間違いないでしょうか。
    また、この方法でアップロードできるファイルサイズの上限はありますか。
    上限を設定、解除できる仕様かどうか公式ドキュメントで言及されている箇所を見つけることができませんでした。
    私は非プログラマなもので、「この方法でアップロードされたファイルがいつ削除されるのか」、「大きいファイルのアップロード中にGASの6分制限に引っかかる恐れがないのか」など根本的な仕組みが分かっていないので、参考になる記述があれば拝読したいです。

    1. officeの杜 より:

      出川様

      Google Apps側で動作してるスクリプトになるので、PCからアップロードする場合は、

      1.クライアント側で一旦読み込み
      2.Blobの形でGAS側に送り込み
      3.GAS上のメモリにロードされてから、処理を開始となります。

      アップロードサイズ上限ですが、記事中終盤にもあるように、50MBが上限となっています。
      通信環境によるので、あまりにも遅いパケット通信の場合は、6分のリミットに掛かる可能性は当然あります。

      そもそもアップロードされたファイルがいつ削除されるか?というのは、ドライブにアップロードした場合は、自身で制限を加えないかぎり、ドライブ上に残ります。

  2. 鈴木翔平 より:

    サンプルコードと丁寧な解説、非常に助かっています。
    同じような事をしたく、サンプルコードを使用させていただいており、
    実際に思いどおりの処理は作成できました。ありがとうございます。
    サンプルコードを自分なりに解析しているのですが、どうしてもわからない点がありまして、
    質問をさせてください。
    【複数ファイルアップローダの場合】のコードで、、、
    ファイル選択ボタンやファイルの選択数などを表示させているのは、どこでやっているのでしょうか?HTMLやJavaなどの知識が浅く、初心者の質問で大変申し訳ございません。

    1. officeの杜 より:

      鈴木さん
      officeの杜管理人です。

      このアップローダはHTMLの標準のinputタグを使って実現してるものなので、ボタンそのものはinputタグの2個目(input type=”button”)
      個数に関してはinputタグの1個目です(input type=”file”)

      これらが自動で作ってくれてるものになります。CSSなどで色々といじれるようです
      https://into-the-program.com/customize-input-type-file/

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)