LoginSignup
1
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-04-10

発注書とかを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

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側コード

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 (ネイティブ)

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

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

なお

    <input name="myFile" type="file" multiple />

とやって複数のファイルを選択しても、GAS側には一つしかファイルは渡らないようです。
今回の案件では、ファイルは1個づつ処理するするので、深堀りは避けました。
(複数ファイルを一括でUPできる簡単なやり方知ってる知ってる方いたら教えてください!)

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

Vue.js CDN版 (2.6.14)

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を使っていますが、私のQiitaの記事ではBuefyが多いので、その例を出します。

Vue+Buefy CDN版

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

幾分それっぽい処理を行いました。
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
おっと…? Readonlyなんすね…

ふむふむ。

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

終わり

閉廷!
以上みんな解散!

1
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
5