Google Apps Scriptで作ったウェブアプリからGoogleドライブにファイルをアップロード

Google Apps Scriptで、ローカルに保存されたファイルをGoogleドライブにアップロードしてサムネイル画像を表示する方法を紹介します。Apps Scriptでウェブアプリを作ったり、DriveAppを使った経験がある人向けに書いています。

出来上がりの画面はこちらです。画面上部の「ファイルを選択」ボタンを押してアップロードするファイルを選び「アップロード」ボタンを押します。少し時間がかかりますが、そのサムネイル画像が画面に表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/6264fd1a-aca6-b526-52b6-4213c3233dda.png)

# ウェブアプリの動作

ファイルのサムネイル画像は、ファイルをアップロードしてから少し時間が経たないと生成されません。
そこでわざと「ファイルのアップロード」と「サムネイル画像の要求」を分けて実施します。

## ファイルのアップロード
- クライアント
  - `<input type="file">`でファイル選択ボタン、`<input type="submit">`でアップロードボタンを表示。
  - アップロードボタンが押されると、`<form onsubmit>`の中でgoogle.script.runを呼び出し、ファイルのデータをサーバーに渡す。
- サーバー
  - 渡されたデータをGoogleドライブに保存して`File`を作り、ファイルIDを返却。

## サムネイル画像の要求
- クライアント
  - ファイルIDをサーバーに渡し、サムネイルを要求。
- サーバー
  - ファイルIDのサムネイルを返却。

# 事前作業
- ファイルのアップロード先となるGoogleドライブのフォルダーIDを調べておきます。
- そのフォルダーをウェブブラウザーで表示したときのURLから分かります。`/folders/`以降のIDです。
- `https://drive.google.com/drive/folders/<FOLDER_ID>`

# スクリプトエディター

```javascript:Code.gs
const FOLDER_ID = 'アップロード先のフォルダーID';

function doGet() {
  return HtmlService.createTemplateFromFile('Index').evaluate();
}

function uploadFile(formObj) {
  if (formObj.blob.length === 0) throw 'ファイルが選択されていません。';
  const file = DriveApp.getFolderById(FOLDER_ID).createFile(formObj.blob);
  return file.getId();
}

function fetchThumbnailDataUrl(fileId) {
  const blob = DriveApp.getFileById(fileId).getThumbnail();
  return `data:${blob.getContentType()};base64,${Utilities.base64Encode(blob.getBytes())}`;
}
```

さらに、HTMLファイルを作ります。

```javascript:Index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top" />
  </head>
  <body>
    <dialog id="myDialog">
      通信中...
    </dialog>
    <form id="myForm" onsubmit="upload(this)">
      <input name="blob" type="file" />
      <button type="submit">アップロード</button>
    </form>
    <img id="thumbnail" />
  <script>
    const dialog = document.querySelector('#myDialog');

    function upload(formObj) {
      dialog.showModal();
      google.script.run.withSuccessHandler(fileId => {
        dialog.close();
        displayThumbnail(fileId);
      }).withFailureHandler(err => {
        dialog.close();
        alert(err);
      }).uploadFile(formObj);
    }

    function displayThumbnail(fileId) {
      dialog.showModal();
      google.script.run.withSuccessHandler(url => {
        dialog.close();
        document.querySelector('#thumbnail').src = url;
      }).withFailureHandler(err => {
        alert(err);
        dialog.close();
      }).fetchThumbnailDataUrl(fileId);
    }

    window.addEventListener('load', () => {
      document.querySelector('#myForm').addEventListener('submit', event => {
        event.preventDefault();
      })
    });
  </script>
  </body>
</html>
```

# ハマったこと

## formをsubmitするとページ遷移してしまう

https://www.bpwebs.com/upload-files-to-google-drive-with-google-apps-script/

を参考にして、formのsubmitイベントに対して、preventDefault()して対応しました。

## Googleドライブで作ったサムネイル画像をそのまま返却しても表示できない

https://stackoverflow.com/questions/70483286/unable-to-run-window-url-createobjecturlblob-to-display-a-png-file

を参考にして、data URL の形式に変換して、クライアントで表示しています。

## できたてのFileはgetThumbnail()がnullになる

わざと「ファイルのアップロード」と「サムネイル画像の要求」を分けているのでめったに発生することはないと思います。もし、リトライ処理を実装するなら、`displayThumbnail()`は次のようになります。

```javascript:Index.html
    function displayThumbnail(fileId) {
      dialog.showModal();
      google.script.run.withSuccessHandler(url => {
        dialog.close();
        document.querySelector('#thumbnail').src = url;
      }).withFailureHandler(err => {
        dialog.close();
        const shouldRetry = confirm(`エラーが発生しました。${err} リトライしますか？`);
        if (shouldRetry) {
          displayThumbnail(fileId);
        }
      }).fetchThumbnailDataUrl(fileId);
    }
```

サーバー側も次のように書いておくほうが親切ですね。

```javascript:Code.gs
function fetchThumbnailDataUrl(fileId) {
  const blob = DriveApp.getFileById(fileId).getThumbnail();
  if (!blob) throw 'サムネイル画像を取得できません。';
  return `data:${blob.getContentType()};base64,${Utilities.base64Encode(blob.getBytes())}`;
}
```