LoginSignup
6
5

More than 1 year has passed since last update.

サードパーティーを用いない Google Apps Script と Javascript によるファイルピッカー

Posted at

概要

この投稿ではサードパーティーを用いない Google Apps Script と Javascript によるファイルピッカーを紹介します。これまでの問い合わせで、開発者側の仕様でサードバーティを使用しないファイルピッカーがあれば便利とのご意見が多かったため、このようなサンプルを作成しました。このファイルピッカーの特徴は、次の通りです。

  1. 特定のフォルダ下のファイルを取得できる。
  2. 自分のアカウントの Google Drive だけでなく、サービスアカウントの Drive も利用できる。

リポジトリはこちらです。

デモ

fig1.gif

使用方法

この場合、サンプルスクリプトを実行するための使用方法です。実際に使用される際は、それに合わせて調整してください。

1. スプレッドシートを新規作成する

このサンプルスクリプトでは、スプレッドシートのサイドバーを利用しますので、新規でスプレッドシートを作成します。そして、スクリプトエディタを開きます。

2. Drive API を有効にする

Advanced Google services で Drive API を有効にしてください。 Ref

3. サンプルスクリプト

Google Apps Script side: Code.gs

下記のスクリプトを Google Apps Script ファイルへコピーペーストしてください。このサンプルでは、自分のGoogle Driveのファイルが表示されます。

function getFiles(e, rootFolderId, mimeType) {
  const accessToken = ScriptApp.getOAuthToken(); // In this case, the files and folders are retrieved from your own Google Drive.
  mimeType = mimeType || "*";
  const data = {};
  const idn = e || "root";
  const url1 = `https://www.googleapis.com/drive/v3/files/${e}?fields=id%2Cname%2Cparents`;
  const folderObj = JSON.parse(
    UrlFetchApp.fetch(url1, {
      headers: { authorization: `Bearer ${accessToken}` },
    }).getContentText()
  );
  if (e == "root") e = folderObj.id;
  data[e] = {
    keyname: folderObj.name,
    keyparent: idn == rootFolderId ? null : folderObj.parents[0],
    files: [],
  };
  let pageToken = "";
  do {
    const q =
      mimeType == "*"
        ? `'${e}' in parents and trashed=false`
        : `'${e}' in parents and (mimeType = 'application/vnd.google-apps.folder' or mimeType = '${mimeType}') and trashed=false`;
    const url2 = `https://www.googleapis.com/drive/v3/files?fields=nextPageToken%2Cfiles%28id%2Cname%2CmimeType%29&q=${encodeURIComponent(
      q
    )}&orderBy=name&pageSize=1000&pageToken=${pageToken}`;
    const res = UrlFetchApp.fetch(url2, {
      headers: { authorization: `Bearer ${accessToken}` },
    });
    const fileList = JSON.parse(res.getContentText());
    if (fileList.files.length > 0) {
      data[e].files = data[e].files.concat(
        fileList.files.map(({ id, name, mimeType }) => ({
          name,
          id,
          mimeType: mimeType == MimeType.FOLDER ? "folder" : mimeType,
        }))
      );
    }
    pageToken = fileList.nextPageToken;
  } while (pageToken);
  return data;
}

// DriveApp.getFiles()  // This is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive.readonly". Also, you can use the scope of "https://www.googleapis.com/auth/drive".

// When the file is selected, this function is run from `work(value)` in Javascript side.
function doSomething(id) {
  // do something
  var res = id;
  return res;
}

// Please run this function.
function main() {
  SpreadsheetApp.getUi().showSidebar(
    HtmlService.createTemplateFromFile("index")
      .evaluate()
      .setTitle("Sample File Picker")
  );
}

HTML & Javascript side: index.html

下記のスクリプトを HTML ファイルへコピーペーストしてください。

<style>
  #filePicker {
    text-align: left;
    width: 95%;
    font-size: 1em;
    margin: auto;
    height: 3em;
  }
</style>

<select id="filePicker"></select>
<div id="result"></div>

<script>
  const rootFolderId = "root"; // Please set the top folder ID here.
  const emimeType = "*"; // In this case, all files are shown.
  //   const emimeType = "application/pdf"; // When you use this line, you can show only files with this mimeType.

  // --- When you want to run the script for the selected file, please modify this function.
  const work = (...args) =>
    google.script.run.withSuccessHandler(output).doSomething(...args);

  function output(res) {
    document.getElementById("result").innerHTML = res;
  }
  // ---

  const alldata = {};
  const getFileList = (...args) =>
    google.script.run.withSuccessHandler(importData).getFiles(...args);
  getFileList(rootFolderId, rootFolderId, emimeType);

  document.getElementById("filePicker").addEventListener("change", function () {
    const { value, options } = this;
    const disp = options[options.selectedIndex].text;
    if (disp.includes("Folder") || disp.includes("../")) {
      while (this.lastChild) this.removeChild(this.lastChild);
      if (alldata[value]) {
        importData({ [value]: alldata[value] });
        return;
      }
      getFileList(value, rootFolderId, emimeType);
      return;
    }
    work(value);
  });

  function importData(e) {
    const key = Object.keys(e)[0];
    if (!alldata[key]) alldata[key] = e[key];
    const select = document.getElementById("filePicker");
    const obj = e[key]["keyparent"]
      ? [
          { text: `./${e[key]["keyname"]}`, value: key },
          { text: "../", value: e[key]["keyparent"] },
        ]
      : [{ text: `./${e[key]["keyname"]}`, value: key }];
    obj.forEach(({ text, value }) => {
      const option = document.createElement("option");
      option.textContent = text;
      option.value = value;
      select.appendChild(option);
    });
    e[key]["files"].forEach(({ mimeType, name, id }) => {
      const option = document.createElement("option");
      option.textContent = mimeType == "folder" ? "[Folder]" + name : name;
      option.value = id;
      select.appendChild(option);
    });
  }
</script>
  • トップフォルダを変更する場合は、const rootFolderId = "root";const rootFolderId = "### folder ID ###";のように変更してください。

  • また、mimeType でフィルタをしたい場合は、const emimeType = "*";const emimeType = "### mimeType ###";に変更してください。

サービスアカウントを使用する場合

サービスアカウントを使用する場合は、Google Apps Script を下記のように変更してください。

// This script is from https://gist.github.com/tanaikech/20ea127a8e23a7c609f8d764c8b7ed7c
function getAccessTokenFromServiceAccount(scopes) {
  const private_key =
    "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n"; // private_key of JSON file retrieved by creating Service Account
  const client_email = "###"; // client_email of JSON file retrieved by creating Service Account
  const url = "https://www.googleapis.com/oauth2/v3/token";
  const header = { alg: "RS256", typ: "JWT" };
  const now = Math.floor(Date.now() / 1000);
  const claim = {
    iss: client_email,
    scope: scopes.join(" "),
    aud: url,
    exp: (now + 3600).toString(),
    iat: now.toString(),
  };
  const signature =
    Utilities.base64Encode(JSON.stringify(header)) +
    "." +
    Utilities.base64Encode(JSON.stringify(claim));
  const jwt =
    signature +
    "." +
    Utilities.base64Encode(
      Utilities.computeRsaSha256Signature(signature, private_key)
    );
  const params = {
    method: "post",
    payload: {
      assertion: jwt,
      grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
    },
  };
  const data = UrlFetchApp.fetch(url, params).getContentText();
  const obj = JSON.parse(data);
  return obj.access_token;
}

function getFiles(e, rootFolderId, mimeType) {
  const scopes = ["https://www.googleapis.com/auth/drive.readonly"];
  const accessToken = getAccessTokenFromServiceAccount(scopes); // In this case, the files and folders are retrieved from Google Drive of the service account.

  mimeType = mimeType || "*";
  const data = {};
  const idn = e || "root";
  const url1 = `https://www.googleapis.com/drive/v3/files/${e}?fields=id%2Cname%2Cparents`;
  const folderObj = JSON.parse(
    UrlFetchApp.fetch(url1, {
      headers: { authorization: `Bearer ${accessToken}` },
    }).getContentText()
  );
  if (e == "root") e = folderObj.id;
  data[e] = {
    keyname: folderObj.name,
    keyparent: idn == rootFolderId ? null : folderObj.parents[0],
    files: [],
  };
  let pageToken = "";
  do {
    const q =
      mimeType == "*"
        ? `'${e}' in parents and trashed=false`
        : `'${e}' in parents and (mimeType = 'application/vnd.google-apps.folder' or mimeType = '${mimeType}') and trashed=false`;
    const url2 = `https://www.googleapis.com/drive/v3/files?fields=nextPageToken%2Cfiles%28id%2Cname%2CmimeType%29&q=${encodeURIComponent(
      q
    )}&orderBy=name&pageSize=1000&pageToken=${pageToken}`;
    const res = UrlFetchApp.fetch(url2, {
      headers: { authorization: `Bearer ${accessToken}` },
    });
    const fileList = JSON.parse(res.getContentText());
    if (fileList.files.length > 0) {
      data[e].files = data[e].files.concat(
        fileList.files.map(({ id, name, mimeType }) => ({
          name,
          id,
          mimeType: mimeType == MimeType.FOLDER ? "folder" : mimeType,
        }))
      );
    }
    pageToken = fileList.nextPageToken;
  } while (pageToken);
  return data;
}

// DriveApp.getFiles()  // This is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive.readonly". Also, you can use the scope of "https://www.googleapis.com/auth/drive".

// When the file is selected, this function is run from `work(value)` in Javascript side.
function doSomething(id) {
  // do something
  var res = id;
  return res;
}

// Please run this function.
function main() {
  SpreadsheetApp.getUi().showSidebar(
    HtmlService.createTemplateFromFile("index")
      .evaluate()
      .setTitle("Sample File Picker")
  );
}

スコープについて

このサンプルスクリプトでは、// DriveApp.getFiles()のコメント行によって、https://www.googleapis.com/auth/drive.readonlyのスコープを自動検出するように設定しています。この場合、UrlFetchAppによって、https://www.googleapis.com/auth/script.external_requestのスコープも自動追加されます。

もしもさらに制限したスコープを使用したい場合は、Manifest ファイル(appsscript.json)へhttps://www.googleapis.com/auth/drive.metadata.readonlyhttps://www.googleapis.com/auth/script.external_requestのスコープを設定すると、このサンプルスクリプトは動作します。

もちろん、選択したファイルを他のスコープを使用するメソッドで使用する場合は、それぞれのスコープを追加してください。

6
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
6
5