GASの関数を外部から実行する

# はじめに

GASはスプレッドシートなどのGoogleWorkpaceをシームレスに操作できて便利でが、通常はGoogleWorkspaceアプリ内でしか呼び出せません。しかし、これはGoogleも織り込み済みのようで、GASには関数を 実行可能API として外部に公開する機能があり、実行可能APIを使えば外部サーバでホスティングされているWebアプリからも間接的にGoogleWorkspaceアプリを操作できるようになります。


# サンプルアプリの概要

この記事ではローカル端末のブラウザからGASの関数を実行するサンプルアプリを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/cb9ee6bc-adcd-f191-6d3a-40bed910df48.png)


# 作業手順

GASの関数を外部から実行する手順を見ていきましょう。

少し長いですが、お付き合いください。


## GAS関数の作成
実行可能APIとしてデプロイして外部から実行できるようにするGASの関数を作成します。

1. 任意のフォルダにGASのプロジェクトを作成

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/3e26ebaf-d877-c27c-f76d-a3e437eb383c.png)
    <br>

1. コード.gsを以下のように編集

   ```javascript
   function getFilesInFolder(folderId) {
      let folder = DriveApp.getFolderById(folderId);
      let files = folder.getFiles();
      let fileNameList = [];
      while (files.hasNext()) {
         let file = files.next();
         fileNameList.push(file.getName());
      }
      return fileNameList;
   }

   function test_getFileInFolder() {
      let res = getFilesInFolder("フォルダID");
      Logger.log(res);
   }
   ```
    
   <details><summary>📝フォルダIDの確認方法</summary>

   フォルダIDとは、Googleドライブ内のフォルダを識別するためのユニークな文字列です。このフォルダIDはGoogleドライブのURLの一部として表示されており、フォルダのURLの「folders/」以降の文字列を見れば確認できます。

   [例] URL = https://drive.google.com/drive/folders/1A2B3C4D5E6F  
   　　⇒ フォルダID = 1A2B3C4D5E6F 
   </details>
   <br>

1. 「フォルダID」を任意のフォルダIDに置き換えて test_getFileInFolder を実行

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/a72d6dd7-ff40-6238-1204-73abfd0f8872.png)
   ※ 初回実行時は権限の確認を求められます


## GCPプロジェクトの設定

### プロジェクトの作成
認証機能を使うためのGoogle Cloudプロジェクトを作成します。

1. [Google Cloudのウェルカムページ](https://console.cloud.google.com/welcome?hl=ja)を開く
    <br>

1. 画面左上の「プロジェクトの選択」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/a74b5b53-e2db-0072-0003-102d7f125745.png)
   <br>

1. 画面右上の「新しいプロジェクト」を選択

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/9378a357-49b5-983a-0fbb-b0984e529154.png)
    <br>


1. 任意のプロジェクト名を設定して「作成」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/6cc92ad3-cdd7-5717-2b51-1995261ef327.png)



### APIの有効化
サンプルアプリで使用するAPIを有効化します。

1. 画面左上のメニューボタンからサイドバーを展開して「APIとサービス」→「ライブラリ」と選択

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/ca1f735e-1a11-eb3f-e7b3-3ebb66b64dac.png)
    <br>
    
1. APIライブラリページが開いたら上部中央の検索欄で「Google Drive API」を検索

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/83bc6699-ffa5-21d5-8d15-8f88eebb250f.png)
    <br>

1. 検索結果の中から「Google Drive API」を選択

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/8f0a654b-47cf-7600-5636-a5d62b0edd5a.png)
    <br>


1. 製品の詳細を確認して「有効にする」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/36af5cbc-1732-227b-fa4e-405d21981c49.png)
    <br>

1. 同様の手順でApps Script APIも有効化

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/2263af7a-659b-91fe-6256-9aa5c22df17b.png)
    <br>


### OAuth認証画面の作成
OAuth同意画面を作成します。

ちなみに、OAuth同意画面はGoogle Drive APIなどのユーザの同意が必要なAPIを使う場合にユーザへ権限の確認を求めるために使用します。

1. 画面左上のメニューボタンからサイドバーを展開して「APIとサービス」→「OAuth同意画面」と選択

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/517715e2-5f69-e78f-3604-28849d0e6a31.png)
    <br>

1. ユーザタイプの選択では「External（外部）」を選択して「作成」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/6f76ac76-88ec-d9eb-f3a9-1e2982c975ea.png)
    <br>

1. アプリの情報を求められたら次の項目を入力して「保存して次へ」をクリック

   - アプリ名：OAuth同意画面に表示されるアプリの名称
   - ユーザーサポートメール：OAuth同意画面に表示される問い合わせ先メールアドレス
   - デベロッパーの連絡先情報：Googleからの通知を受け取るメールアドレス

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/a8d824ca-b615-dd32-2c88-4fc012a572e3.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/e56e58f3-a8ec-4d00-ebdb-4b88b8b280f1.png)
    <br>
    
1. 「スコープを追加または削除」をクリックして「フィルタ」欄に「 https://www.googleapis.com/auth/drive.readonly 」入力し、必要なスコープを選択したら「更新」をクリックして「保存して次へ」を選択

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/a0d4bef5-db42-ff38-b52c-2c16312d3dbf.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/2c79a8c5-a11e-c764-59cc-dc1250b187af.png)
   ※ 実行可能APIに必要なスコープはApps Scriptプロジェクトの「概要」画面下部の「プロジェクトの OAuth スコープ」欄で確認できます
    <br>

1. テストユーザの追加画面で「ADD USERS」をクリックしてテストを実施するユーザのメールアドレスを入力し、「追加」をクリックして「保存して次へ」を選択

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/be7c0be0-497d-4131-c4bf-95412dce8b6f.png)


### 認証情報の作成
サンプルアプリ内でユーザを認証するための情報を作成します。

1. サイドバーの「認証情報」から「認証情報を作成」→「OAuthクライアントID」と選択

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/92e7bd54-3720-dd59-ac78-9776562a408d.png)
    <br>

1. 「アプリケーションの種類」を「ウェブアプリケーション」に設定

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/7cab7d14-f5f1-c382-ab76-15caef9d28e8.png)
    <br>

1. 「承認済みのJavaScript生成元」欄に「 http://localhost:9999 」と入力して「作成」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/e817473f-91a4-1b52-273b-b6bf278563cb.png)

   <details><summary>📝URIの設定について</summary>

   「認証済みのJavaScript生成元」のURLはドメインを設定する必要があります。
   そのため、今回は http://localhost を設定しましたが、localhostではどの端末からでも接続できてしまうので、このサンプルではポート番号（:9999）を使用して接続経路を絞り込んでいます。
   
   ポート番号は65535までの番号を指定できるので、不安がある方はポート番号を変更してテストしてみてください。
   </details>
   <br>

1. 「クライアントID」が表示されたらコピーして「OK」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/56f3f6e0-0b23-fa95-b5c5-b538d7519c8c.png)


### APIキーの作成

1. 「認証情報」画面から「認証情報を作成」→「APIキー」と順に選択

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/d898ad40-78c2-7f3f-68ac-98e63291c769.png)
    <br>

2. APIキーが表示されたらコピーして「閉じる」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/647db4a7-4a0f-e7af-5cfb-71cf48ca703e.png)


## GASのプロジェクトの設定

### GASとGCPを紐づける

1. 画面上部ののプロジェクト名を確認して左上メニューボタンからGCPプロジェクトのダッシュボードを開く

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/4f8f0bcd-f021-0bf5-6e9d-9b38b0ce4c99.png)
    <br>

1. GCPプロジェクトのダッシュボードで「プロジェクト番号」をコピー

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/8169ec45-0946-ed72-ad48-7ef86baf9d3d.png)
    <br>

1. GASのプロジェクトの設定画面を開く

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/83a4e6a9-0350-6f81-9b08-13439686dcb8.png)
    <br>

1. GASのプロジェクトの設定画面のGoogle Cloud Platform (GCP) プロジェクト欄の「プロジェクトを変更」ボタンをクリック

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/e40eec1a-74b4-5311-1ddd-7fff357f79e3.png)
    <br>

1. プロジェクト番号を入力して「プロジェクトを設定」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/887da129-d2d7-3b38-7627-b5d3ba949368.png)


### 実行可能APIをデプロイする

1. GASのプロジェクト画面右上の「デプロイ」から「新しいデプロイ」を選択

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/9f4a7a8e-2a87-41f2-c8ba-a432eb5e81aa.png)
    <br>

1. 新しいデプロイの設定画面でデプロイの種類を「実行可能API」に設定

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/8ae1655e-72d5-01b9-acec-b3612b0cbe01.png)
    <br>

1. デプロイの詳細を以下のように入力して「デプロイ」をクリック

   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/ae7e4628-870a-dcff-2d45-b83ea19f765d.png)
    <br>

1. デプロイが完了したら「デプロイID」をコピーして「閉じる」を選択

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/dcd46cec-8104-b381-c0f8-8411dfce615f.png)
   (※このデプロイIDは「サンプルスクリプトの作成」で使います)


## サンプルアプリの作成

### index.htmlの作成

1. 任意の作業フォルダにindex.htmlを作成

1. index.htmlの内容を以下のように編集

   ```html
   <!DOCTYPE html>
   <html>
   <head>
      <title>実行可能APIクイックスタート</title>
      <meta charset="utf-8" />
      <style>
         body {
            background-color: black;
            color: white;
         }
      </style>
   </head>
   <body>
      <h1>実行可能APIテスト</h1>

      <button id="run_button" onclick="handleRunClick()" hidden>実行</button>
      <button id="reset_button" onclick="handleResetClick()" hidden>リセット</button>

      <ol id="content"></ol>

      <script type="text/javascript">

         const CLIENT_ID = "クライアントID"; // TODO: GCPで取得したクライアントIDを貼付
         const API_KEY = "APIキー"; // TODO: GCPで取得したAPIキーを貼付
         const SCRIPT_ID = 'デプロイID'; // TODO: GASで取得したデプロイIDを張付
         const FOLDER_ID = "フォルダID"; // TODO: GoogleドライブのフォルダIDを貼付
         const SCOPES = "https://www.googleapis.com/auth/drive";

         let tokenClient;
         let gapiInited = false;
         let gisInited = false;

         // Google APIクライアントライブラリの初期化
         function gapiLoaded() {
            gapi.load("client", async () => {
               await gapi.client.init({
                  apiKey: API_KEY,
                  discoveryDocs: ["https://script.googleapis.com/$discovery/rest?version=v1"],
               });
               gapiInited = true;
               maybeEnableButtons();
            });
         }
         
         // Google Identity Serviceクライアントライブラリの初期化
         function gisLoaded() {
            tokenClient = google.accounts.oauth2.initTokenClient({
                  client_id: CLIENT_ID,
                  scope: SCOPES,
                  callback: "",
            });
            gisInited = true;
            maybeEnableButtons();
         }

         // Google APIライブラリとGISライブラリの初期化が両方完了したら認証ボタンを表示
         function maybeEnableButtons() {
            if (gapiInited && gisInited) {
                  document.getElementById("run_button").hidden = false;
            }
         }

         // 実行ボタンがクリックされた時の処理
         function handleRunClick() {
            if (gapi.client.getToken() === null) {
               // トークンを取得していない場合の処理
               tokenClient.callback = async (resp) => {
                     if (resp.error !== undefined) throw (resp);
                     document.getElementById("reset_button").hidden = false;
                     document.getElementById("run_button").innerText = "更新";
                     await callScriptFunction();
               };
               tokenClient.requestAccessToken({
                  prompt: "consent"
               });
            } else {
               // トークを取得している場合の処理
               tokenClient.callback = async (resp) => {
                     if (resp.error !== undefined) throw (resp);
                     document.getElementById("content").innerHTML = "";
                     await callScriptFunction();
               };
               tokenClient.requestAccessToken({
                  prompt: ""
               });
            }
         }

         // リセットボタンがクリックされた時の処理
         function handleResetClick() {
            const token = gapi.client.getToken();
            if (token !== null) {
               google.accounts.oauth2.revoke(token.access_token);
               gapi.client.setToken("");
               document.getElementById("content").innerHTML = "";
               document.getElementById("run_button").innerText = "実行";
               document.getElementById("reset_button").hidden = true;
            }
         }

         // 実行可能APIを呼び出す処理
         async function callScriptFunction() {
            try {
               gapi.client.script.scripts.run({
                  'scriptId': SCRIPT_ID,
                  'resource': {
                     'function': 'getFilesInFolder',
                     'parameters': [FOLDER_ID],
                  },
               }).then(function(resp) {
                  const result = resp.result;
                  if (result.error && result.error.status) {
                     // API実行前にエラーになった場合の処理
                     console.error('Error calling API:');
                     console.error(JSON.stringify(result, null, 2));
                  } else if (result.error) {
                     // API実行中にエラーになった場合の処理
                     const error = result.error.details[0];
                     console.error('Script error message: ' + error.errorMessage);
                     if (error.scriptStackTraceElements) {
                        console.error('Script error stacktrace:');
                        for (let i = 0; i < error.scriptStackTraceElements.length; i++) {
                           const trace = error.scriptStackTraceElements[i];
                           console.error('\t' + trace.function + ':' + trace.lineNumber);
                        }
                     }
                  } else {
                     // 実行可能APIからのレスポンスを処理
                     const fileList = result.response.result;
                     if (fileList.length == 0) {
                        console.warn('ファイルが見つかりませんでした');
                     } else {
                        fileList.forEach((file) => {
                           li = document.createElement("li");
                           li.textContent = file;
                           document.getElementById('content').appendChild(li);
                        });
                     }
                  }
               });
            } catch (err) {
               document.getElementById('content').innerText = err.message;
               return;
            }
         }
      </script>
      <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
      <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
   </body>
   </html>
   ```

## サンプルアプリのテスト

### ローカルサーバの起動
   1. index.htmlが置かれているディレクトリで下記のコマンドを実行

      ```sh
      npm install http-server
      ```
      <br/>

   1. 次のコマンドでローカルサーバを起動

      ```sh
      npx http-server -p 9999
      ```
      <br/>

### 動作確認

   1. ブラウザで http://localhost:9999 にアクセス

        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/06e79f57-2657-03f8-b98c-0efcff435d35.png)
        <br>

   1. 「実行」ボタンをクリックするとOAuth認証画面が開くので実行するアカウントを選択する

        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/d0f69728-8f2d-ea2b-8c68-6346c4264ad9.png)
        <br>

   1. 「このアプリはGoogleで確認されていません」などのメッセージが出ますが「続行」を選択

        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/44eebcfa-96a0-1190-30b4-e082d43543a5.png)
        <br>

   1. スコープの利用について同意を求められたら内容を確認して「続行」を選択

        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/20d3cc9e-d2a7-42c3-6c8f-8a391203379e.png)
        <br>

   1. 少し待って下図のようにファイル名の一覧が表示されれば成功です！！

        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/38932be4-2bf2-b15a-969e-b6b937409c01.png)