GoogleAppsScriptのウェブアプリからGoogleDriveへのファイルアップローダーを作る

# 発注書とかをUploadしてもらってゴニョゴニョしたい！

PDFで送られてくる発注書や請求書とかの電子書類を処理するために、社内のGoogle Workspaceメンバー向けの仕組みを作る事になりました。
幾つかやり方があったけど、今回は

* GASによるWevアプリから
* Vue.jsを使ったアップローダーで
* GoogleDriveにアップロードしたり
* AppScriptで各種自動化処理をする

という方向で進める事としたのだが、軽く躓いた箇所があったので備忘録的に記録に残す事とします。

## 基本事項

* Google Workspace for Business　（無料のgmailでもOK）
* Google App Script (V8)
* Vue.js(2.6.14) CDN版
* Google Chrome
* GASによるWevアプリを作った事がある人向けの解説

## manifest

```json:appscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "DOMAIN"
  }
}
```

appscript.jsonですが、アップロード先を集中管理したい関係で executeAs は USER_DEPLOYING に。
access を DOMAIN としていますが、アクセス制限要らないなら ANYONE_ANONYMOUS で良いです。

## GAS側コード
```js:code.gs
// doGet https://developers.google.com/apps-script/guides/web
// HtmlService https://developers.google.com/apps-script/reference/html/html-service
function doGet() {
  const htmlOutput = HtmlService.createTemplateFromFile("native").evaluate();
  // const htmlOutput = HtmlService.createTemplateFromFile("vue").evaluate();
  // const htmlOutput = HtmlService.createTemplateFromFile("buefy").evaluate();

  htmlOutput
    .setTitle("GASのウェブアプリでファイルをアップロード")
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')

  return htmlOutput;
}

// formとして送られたfile属性のデータは、Apps Scriptの class Blob として渡されるっぽい
// https://developers.google.com/apps-script/reference/base/blob
function gasUpload(formObject) {
  try {
    const file = formObject.myFile;
    console.log(`name:${file.getName()}\ntype:${file.getContentType()}`);

    if (!file.getName()) throw new Error("ファイルが設定されていません");

    const folderID = 'oOxxOoOoXOoXoxxOoXOoxOxxOXoxxOoxooo';

    const uploadFolder = DriveApp.getFolderById(folderID);
    const uploadFile = uploadFolder.createFile(file)

    return uploadFile.getDownloadUrl()
  } catch (e) {
    console.log(e)
    throw e;
  }
}

```

doGetはいつも通りです。
実行の際は以下のサンプルに応じてnative / vue / buefyからテキトーに選んでください。

function gasUpload(formObject) が google.script.run から呼ばれるヤツです。
引数はformから送られたオブジェクトが何かゴニョゴニョ加工されてGASに渡ってきます。

今回はmyFileにFileが送られてくる事を決め打ちで処理していまが、社外用に使う場合はもっと厳密な処理をした方が良いでしょう。

## HTML (ネイティブ)
```html:native.html
<!DOCTYPE html>
<html>
<body>
  <p>GASでUpload native</p>
  <form id="myForm">
    <input name="myFile" type="file" />
    <!-- ここのnameでGAS側からfileをアクセスする -->
  </form>

  <button onclick="onClick()">upload</button>
  <!-- 配置の関係でformからはsubmitせず、ボタンから操作させる -->

  <script>
    function onClick() {
      const form = document.getElementById("myForm");
      google.script.run
        .withSuccessHandler(onSuccess)
        .withFailureHandler(onFailure)
        .gasUpload(form);
    }

    function onFailure(e) {
      console.log(e.message);
      alert(e.message);
    }

    function onSuccess(url) {
      console.log(url);
      alert("アップロードが成功しました\n"+url);
    }
  </script>
</body>
</html>
```

まず、Vue.jsを使わないnativeなHTML+Javascriptの例を出します。

実行画面
>![2022-04-10_16h24_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14114/fa627c92-e388-3556-458d-54976c34dd8a.png)

GASのWebアプリを使ってファイルをアップロードする場合、google.script.runでGASに送るオブジェクトとしてformをマルっと送ると、何か謎のGoogleパワーで良い感じにスクリプトに送ってくれます。

なお
```javascript
    <input name="myFile" type="file" multiple />
```
とやって複数のファイルを選択しても、GAS側には一つしかファイルは渡らないようです。
今回の案件では、ファイルは１個づつ処理するするので、深堀りは避けました。
（複数ファイルを一括でUPできる簡単なやり方知ってる知ってる方いたら教えてください！）

またこの例では、form から submitせず、わざわざボタン操作させてgetElementById("myForm")しています。
これは、この後のデザインの関係で、アップロードボタンを離した位置に配置したかったためこうしました。
native Javascriptだとbuttonをdisableにしたりするの面倒なので、そういうのはVue.jsとか使ってやります。
GASによるUploaderの基本構造としてはこういう感じになるんだと思って頂ければOKです。
（というか自分が理解するために書いたってのはあるある）

## Vue.js CDN版 (2.6.14)
```html:vue.html
<!DOCTYPE html>
<html>
  <body>
    <div id="app">
      <p>{{title}}</p>
      <form id="myForm">
        <input name="myFile" type="file" />
        <!-- ここのnameでGAS側からfileをアクセスする -->
      </form>
      <button @click="onClick()">upload</button>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

    <script>
      new Vue({
        el: "#app",
        data() {
          return {
            title: "GASでUpload with vue",
          };
        },
        methods: {
          onClick() {
            const form = document.getElementById("myForm")
            google.script.run
              .withSuccessHandler(this.onSuccess)
              .withFailureHandler(this.onFailure)
              .gasUpload(form);
          },

          onFailure(e) {
            console.log(e.message);
            alert(e.message);
          },

          onSuccess(url) {
            console.log(url);
            alert("アップロードが成功しました\n"+url);
          }
        },
      });
    </script>
  </body>
</html>
```

NativeJavascriptをVue.js 2系に置き換えたシンプルな構成です。
DOM操作で form を取得していますが、多分 this.$refs からでもOKです。

別段変な事はしていませんので、Vue.js 3.0系のComposition APIでも大丈夫かと思います。
VueのUIフレームワークはまだまだ2.0系が多いです。

普段は[Vuetify](https://vuetifyjs.com/ja/)を使っていますが、私のQiitaの記事では[Buefy](https://buefy.org/)が多いので、その例を出します。

## Vue+Buefy CDN版

```html:buefy.html
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="https://unpkg.com/buefy/dist/buefy.min.css" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@5.8.55/css/materialdesignicons.min.css" />
  </head>

  <body>
    <div id="app">
      {{title}}
      <section>
        <form id="myForm">
          <b-upload name="myFile" v-model="dropFiles" multiple drag-drop>
            <section class="section">
              <div class="content has-text-centered">
                <p>
                  <b-icon icon="upload" size="is-large" />
                </p>
                <b-tag v-if="uploadFilename">{{uploadFilename}}</b-tag>
                <p>ここにファイルをドロップ</p>
              </div>
            </section>
          </b-upload>
        </form>
        <b-button :loading="uploading" :disabled="!uploadFilename" @click="onClick">upload</b-button>
      </section>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script src="https://unpkg.com/buefy/dist/buefy.min.js"></script>

    <script>
      new Vue({
        el: "#app",
        data() {
          return {
            title: "GASでUpload with buefy",
            uploading: false,
            dropFiles: []
          };
        },
        computed: {
          uploadFilename() {
            return this.dropFiles.length ? this.dropFiles[0].name : null
          }
        },
        methods: {
          loading(sw) {
            this.uploading = sw;
          },
          deleteDropFile(index) {
            this.dropFiles.splice(index, 1)
          },
          onClick() {
            this.loading(true);
            const form = document.getElementById("myForm")
            google.script.run
              .withSuccessHandler(this.onSuccess)
              .withFailureHandler(this.onFailure)
              .withUserObject(this)
              .gasUpload(form);
          },

          onFailure(e) {
            this.loading(false);
            console.log(e.message);
            alert(e.message);
          },

          onSuccess(url) {
            this.loading(false);
            this.deleteDropFile(0);
            console.log(url);
            alert("アップロードが成功しました\n"+url);
          }
        },
      });
    </script>
  </body>
</html>
```

実行画面
>![2022-04-10_16h23_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14114/f548156e-9522-9316-39d8-a88ef9231a19.png)

幾分それっぽい処理を行いました。
buefyのb-uploadは　＜input type="file"＞　をベースにしているっぽいので、nameを付けてformから送ってあげればちゃんとGASで処理されます。

ただ、このソースにはバグが残っています。
OpenFileDialog経由でアップしたファイルは正しく処理されるのですが、「ここにドロップ」からD&Dでファイルを突っ込むと上手い事ファイルが渡せません。
（GASから見ると、引数にFileの実態が無いPOSTが渡されてくる）
しょうがないのでinputにFileオブジェクトを渡そうとv-modelで突っ込んでみたのですが
![2022-04-10_16h31_42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14114/478f6536-28ba-5285-cb06-71e31c91d1f8.png)
おっと…？　Readonlyなんすね…

https://qiita.com/after666/items/0b9503e8f3026cb1d686

ふむふむ。

UIフレームワークで「ここにドロップ」的なインターフェイスが提供されていない事がある理由って、この辺に何か問題があるんでしょかね？
GASのウェブアプリはサンドボックスが強烈な環境なので、この見た目のインターフェイスは解決策は深堀しない事としました。
（誰かうまいやり方知ってたら教えてください！）

## 終わり
閉廷！
以上みんな解散！