発注書とかを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
{
"timeZone": "Asia/Tokyo",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"webapp": {
"executeAs": "USER_DEPLOYING",
"access": "DOMAIN"
}
}
appscript.jsonですが、アップロード先を集中管理したい関係で executeAs は USER_DEPLOYING に。
access を DOMAIN としていますが、アクセス制限要らないなら ANYONE_ANONYMOUS で良いです。
GAS側コード
// 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 (ネイティブ)
<!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の例を出します。
実行画面
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)
<!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版
<!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>
実行画面
幾分それっぽい処理を行いました。
buefyのb-uploadは <input type="file"> をベースにしているっぽいので、nameを付けてformから送ってあげればちゃんとGASで処理されます。
ただ、このソースにはバグが残っています。
OpenFileDialog経由でアップしたファイルは正しく処理されるのですが、「ここにドロップ」からD&Dでファイルを突っ込むと上手い事ファイルが渡せません。
(GASから見ると、引数にFileの実態が無いPOSTが渡されてくる)
しょうがないのでinputにFileオブジェクトを渡そうとv-modelで突っ込んでみたのですが
おっと…? Readonlyなんすね…
ふむふむ。
UIフレームワークで「ここにドロップ」的なインターフェイスが提供されていない事がある理由って、この辺に何か問題があるんでしょかね?
GASのウェブアプリはサンドボックスが強烈な環境なので、この見た目のインターフェイスは解決策は深堀しない事としました。
(誰かうまいやり方知ってたら教えてください!)
終わり
閉廷!
以上みんな解散!