Google Web APIを使用した外部フォームからのファイルアップロード

# 概要

ここではGoogle Web APIを使用したファイルアップロードについて記載します。次の2種類の方法をご紹介します。

1. GASプロジェクトで作成したフォームからのファイルアップロード

2. 外部フォームからのファイルアップロード

## 経緯

GASプロジェクトで作成したフォームを使用した場合は、[説明](https://developers.google.com/apps-script/guides/content)通りに進めることで特に問題なくファイルのアップロードはできるのですが、外部に設置したフォームからファイルをアップロードしようとするとハマってしまいました。これまではGASプロジェクトでのみ使用していましたので気にならなかったのですが、いざローカルからアップロードしようとするとできません。具体的にはGAS側のdoPost()で受けるe.parameterにファイルが現れません。もしかすると、GASのWeb APIは外部からのmultipartでのPOSTができないのではないかと考えました。これについて公式でmultipartでのPOSTはできないといった内容は認められませんでしたが、調べてみると、同じところでハマっているといった投稿がいくつかあり、それに対しての解決策は以前はできていたが今はできないっぽい？ようなあやふやな状況で、残念ながら具体策は見当たりませんでした。

そこで、multipartでの送信を諦めて、multipartがだめならmultipartでないPOSTでファイルを送信してはどうかと考えました。文字列は送ることができますので、これを使ってファイルを送信することにしました。昔使っていたパケット通信を思い出しました。当時使用していたプロトコルではバイナリデータを転送することができなかったため、一度テキストデータに変換してから送信していたことを思い出しました。そのときに使用していたのはishです。ishとはバイナリデータとテキストデータを相互に変換することのできるソフトウェアです。テキストを送ることができるのであればこれと同じようなことをすることでバイナリデータ（今回の場合は画像データ）を送ることができるのではないかと考えました。

そこで、画像データを[GASのツール](https://developers.google.com/apps-script/reference/utilities/utilities#base64Decode(String))にもあるBase64にエンコードしてmultipartを使用せずに文字列としてデータ送信し、届いたデータをデコードして画像データに復元するプロセスを採用しました。結果は無事に転送することができました。動作テストでは3 kb程度の小さな画像データでの確認のため、データサイズの制限は分かりませんが、制限があった場合でも分割して送信し、あとから結合する方法もあります。これもパケット通信でのテクニックです。Drive APIを使うとmultipartでの送信ができますので、本方法は実験的なものと位置付けたいと思います。

# 準備

ここでご紹介する内容はGASプロジェクトを開いた際の上部メニューにある「公開」「WEBアプリケーションとして導入」によりWEB APIを使用できる状況下での方法です。WEBアプリケーションとして導入するために次のような操作を行います。

1. GASプロジェクトの上部メニューにある「公開」「WEBアプリケーションとして導入」を開く。

2. プロジェクトバージョンは新規作成で最新にする。

3. 次のユーザーとしてアプリケーションを実行は自分を選択

4. アプリケーションにアクセスできるユーザーは全員（匿名ユーザーを含む）を選択

5. 現在のウェブアプリケーションのURLをコピーしておく。（スクリプトで使用します）

# 1. GASプロジェクトで作成したフォームからのファイルアップロード

下記スクリプトを使用する手順は次の通りです。

1. GASプロジェクトを新規作成

2. プロジェクト内でHTMLファイルを新規作成し、下記HTMLを貼り付けてForm.htmlとして保存

3. GASプロジェクト作成時に最初からあるコード.gsへ下記スクリプトを貼り付け

4. 先ほどコピーした現在のウェブアプリケーションのURLをブラウザへ入力することでフォームを起動


動作内容は、画像ファイルを選択してOKボタンを押すと、ファイルがアップロードされ、Google Driveのルートに保存されます。とてもシンプルなスクリプトでファイルのアップロードができます。

**Form.html :**

```html:
<html>
  <body>
    <form>
      <input type="file" name="imageFile">
      <input type="button" value="ok" onclick="google.script.run.upload(this.parentNode)">
    </form>
  </body>
</html>
```

**GAS :**

```gas:
function doGet() {
  return HtmlService.createHtmlOutputFromFile('Form.html');
}

function upload(e) {
  DriveApp.createFile(e.imageFile);
}
```

# 2. 外部フォームからのファイルアップロード

GASのスクリプトは次の通りです。サンプルとしてpngファイルを受けるようにしています。使用できるmimetypeの種類については[こちら](https://developers.google.com/apps-script/reference/base/mime-type)を参考にしてください。動作内容は1のときと同じで、受け取ったbase64のデータをデコードして画像ファイルに復元している点で異なります。fileはbase64にエンコードした文字列データ、filenameはGoogle Driveへ保存する際のファイル名です。スクリプトを用意した後、上にある準備の項目を実行してください。

```gas:
function doGet(e) {
  return message("Error: no parameters");
}

function doPost(e) {
  if (!e.parameters.filename || !e.parameters.file) {
    return message("Error: Bad parameters");
  } else {
    var data = Utilities.base64Decode(e.parameters.file, Utilities.Charset.UTF_8);
    var blob = Utilities.newBlob(data, MimeType.PNG, e.parameters.filename);
    DriveApp.createFile(blob);
    return message("completed");
  }
}

function message(msg) {
  return ContentService.createTextOutput(JSON.stringify({result: msg})).setMimeType(ContentService.MimeType.JSON);
}
```

このスクリプトの動作確認のために使用した送信側のcurlのサンプルを使って説明します。

```
curl -L \
    -F 'filename=[Google Driveへ保存する際のファイル名]' \
    -F "file=`base64 [upload file]`" \
    '[Current web app URL (https://script.google.com/macros/s/[Project ID]/exec)]'
```

このサンプルはBash上で動作確認をしています。Windows DOSで行う場合はbase64ではなく、certutilを使うことになるかと思われます。その際は、ヘッダとフッダは削除して使用するようにしてください。

curlでのmultipart POSTは"-F file=@ファイル名"の場合になります。この方法ではdoPostのe.parameterには現れませんので、multipartでないPOSTとして"-F file="のようにし、そこへbase64に変換した文字列を代入します。受け手側のGASでは通常のテキストデータを受けるのと同様に"e.parameters.file"として受け取り、デコードしてバイナリデータに復元します。

curlでのテストで無事に画像データを送信することができましたので、この結果を用いることで下記のようなHTMLフォームからもファイルアップロードができるようになりました。

```html:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="http://code.jquery.com/jquery-latest.js" type="text/javascript"></script>
</head>
<body>
    <input type="file" id="file">

    <script type="text/javascript">
        $(function(){
            var url = 'https://script.google.com/macros/s/[Project ID]/exec';
            var params = {
                filename: 'samplefile',
                imageformat: 'PNG'
            };

            $('#file').on("change", function() {
                var file = this.files[0];
                var fr = new FileReader();
                fr.onload = function(e) {
                    params.file = e.target.result.replace(/^.*,/, '');
                    postJump();
                }
                fr.readAsDataURL(file);
            });

            function postJump(){
                var html = '<form method="post" action="'+url+'" id="postjump" style="display: none;">';
                Object.keys(params).forEach(function (key) {
                    html += '<input type="hidden" name="'+key+'" value="'+params[key]+'" >';
                });
                html += '</form>';
                $("body").append(html);
                $('#postjump').submit();
                $('#postjump').remove();
            }
        });
    </script>
</body>
</html>
```

サンプルのpostJump()は、[tsunet111氏の記事](http://qiita.com/tsunet111/items/0167704a65ebc04411f9)を参考にさせていただきました。

動作内容は、画像ファイル（ここではpngファイル）を選択するとbase64に変換したデータを送信します。{"result":"completed"}が表示されると送信完了です。送信したデータは"filename"で設定したファイル名でGoogle Driveのルートへ保存されます。画像として認識されていると、アイコンが画像の内容に変化するかと思います。