LoginSignup
1
0

More than 1 year has passed since last update.

Google Drive™ API を活用した簡単な共有アプリの作り方

Last updated at Posted at 2022-08-06

概要

  • Google Drive APIを使えば、無料で数十人程度の情報共有アプリを(Google審査以外は)簡単に作れる
  • Google Drive標準GUIに乗っかれば、共有関連UIも作成不要
  • この記事を参考アプリを作ってGoogleにOAuth申請をする際には、できれば最後に記載の要望を上げてほしい

なぜ簡単に作れるか?

Google Drive上のユーザの手作業で共有関連処理を行えるようにしているからです。以下、自作アプリ共有機能紹介ページからの説明画像の引用です。

image.png

共有対象を探して設定してという様な情報をユーザとやり取りするのは面倒ですが、全てGoogle Driveが代わりにやってくれる流れになっています。このため、アプリ固定ファイル名のファイル作成・更新・読み込み・一覧という最低限のAPI利用で、共有アプリに必要な情報交換ができてしまいます。

Google Drive APIとは?

Google Drive内のファイルアクセスをプログラムから自動で行うためのAPIです。ここでリストアップされている様に、Google Driveを使ってユーザができる事はだいたいできます。

ファイルには個人的な共有したくない情報も入っていますが、Google Drive APIは野放しに使われると、その様な情報も含め漏れ放題になってしまいます。

このため、Googleは色々対策をしており、危険な情報にアクセスできるほど厳しいチェックが入ります。残念ながら、今回紹介する簡易共有アプリの作り方だと、Google Driveへのフルアクセスを必要とするため、申請時に厳しいチェックが入ります。(個人や限られたグループ内だけで使うなら、審査不要です。)

以下のGoogle Drive APIコードを見ると分かる通り、アプリ固定ファイル名以外には一切触れない様にはなっています。が、あくまで自主規制なので、悪いことをしようと思えば(そしてGoogleの審査が無ければ)プライバシーダダ洩れにできてしまいます。この懸念に対応するため、共有機能紹介ページでは、以下の様に専用アカウントを作る案内もしています。

image.png

Google Drive APIを使ったデータ共有コード

以下、クロム拡張を前提としたコードにはなっていますが、GoogleDriveApi.getParamsだけフレームワークに合わせて取り換えれば、他は流用できるはずです。

GoogleDriveApi.ts
// 自分のアプリで同期に使う、他と被りそうにないファイル名
// なお、以下ではファイル内容はJSON形式を前提としているので
// 違う場合は適宜ファイルタイプなどを調整してください
const GD_FILE_NAME = "My_App_Sync_Data.dat";

// Googleドライブから取得した同期に必要なファイル情報
export class GoogleDriveFileInf {
  constructor(
    // ファイルID
    readonly id: string,
    // ファイルバージョン(更新チェック用)
    readonly ver: number,
    // ファイル更新日時(更新チェック用)
    readonly updateTime: Date,
    // 所有者名(他ユーザの目視判別用)
    readonly ownerName: string,
    // 所有者メール(他ユーザの機械的判別用・所有者名は同じ名前にできるため)
    readonly ownerMail: string,
    // 所有者が自分かどうか
    readonly ownerIsMe: boolean,
    // 自分に共有されたか?(不正なファイルの所有者を押し付けられる事故防止用)
    readonly sharedWithMe: boolean
  ) {}

  static async fromObj(
    // Googleドライブから取得した結果JSON内のファイル情報オブジェクト
    obj: any,
    // 所有者名とメールを暗号化する変換器を外部から提供
    nameMailConv: (src: string) => Promise<string>,
    // ファイルIDが決まっている場合は外部から提供
    id?: string
  ): Promise<GoogleDriveFileInf> {
    try {
      if (id == null) {
        id = obj.id;
      }
      const version = parseInt(obj.version, 10);
      const updateTime = new Date(obj.modifiedTime);
      if (
        typeof id === "string" &&
        obj.owners instanceof Array &&
        obj.owners.length > 0 &&
        typeof obj.owners[0].displayName === "string" &&
        typeof obj.owners[0].emailAddress === "string" &&
        typeof obj.owners[0].me === "boolean"
      ) {
        return new GoogleDriveFileInf(
          id,
          version,
          updateTime,
          await nameMailConv(obj.owners[0].displayName),
          await nameMailConv(obj.owners[0].emailAddress),
          obj.owners[0].me,
          obj.sharedWithMeTime != null
        );
      }
    } catch {}
    throw new Error("Invalid obj");
  }
}

// GoogleドライブAPIで同期をするための必要最低限処理をまとめたクラス
export class GoogleDriveApi {
  // アクセスのためのOAuthトークンをchrome identity APIを通して取得
  // コード以外の準備は https://developer.chrome.com/docs/extensions/mv3/tut_oauth/
  private static async getParams(
    method: string = "GET",
    contentType: string = "application/json"
  ): Promise<RequestInit> {
    return new Promise((resolve, _reject) => {
      chrome.identity.getAuthToken({ interactive: true }, (token) => {
        const init = {
          method: method,
          async: true,
          headers: {
            Authorization: "Bearer " + token,
            "content-type": contentType,
          },
        };
        resolve(init);
      });
    });
  }

  // 指定した内容で、アプリ固定ファイル名のファイルをGoogleドライブに作る
  static async createFile(
    text: string,
    nameMailConv: (src: string) => Promise<string>
  ): Promise<GoogleDriveFileInf> {
    // 参考:https://qiita.com/comefigo/items/37e0e7668166d494b922
    // randamUUIDが衝突する確率は、隕石が衝突する程度で、事実上衝突しないものとして扱える
    const boundary = "---" + crypto.randomUUID() + "---";
    const delimiter = "\r\n--" + boundary + "\r\n";
    const closeDelim = "\r\n--" + boundary + "--";
    const init = await GoogleDriveApi.getParams(
      "POST",
      'multipart/related; boundary="' + boundary + '"'
    );
    const metadata = {
      name: GD_FILE_NAME,
      mimeType: "application/json",
    };
    const multiPartBody =
      delimiter +
      "content-type: application/json; charset=UTF-8\r\n\r\n" +
      JSON.stringify(metadata) +
      delimiter +
      "content-type: application/json; charset=UTF-8\r\n\r\n" +
      text +
      closeDelim;
    init.body = multiPartBody;
    const res = await fetch(
      "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,version,modifiedTime,owners,sharedWithMeTime",
      init
    );
    return GoogleDriveFileInf.fromObj(await res.json(), nameMailConv);
  }

  // 指定した内容で、指定したIDのGoogleドライブファイルの内容を更新する。
  // IDは、後述のqueryFilesで取得した値を使う。
  static async updateFile(
    text: string,
    nameMailConv: (src: string) => Promise<string>,
    id: string
  ): Promise<GoogleDriveFileInf> {
    const init = await GoogleDriveApi.getParams("PATCH");
    init.body = text;
    const res = await fetch(
      "https://www.googleapis.com/upload/drive/v3/files/" +
        encodeURI(id) +
        "?uploadType=media&fields=version,modifiedTime,owners,sharedWithMeTime",
      init
    );
    return GoogleDriveFileInf.fromObj(await res.json(), nameMailConv, id);
  }

  // 指定したIDのファイル内容を取得する。
  // IDは、後述のqueryFilesで取得した値を使う。
  static async readTextFromFile(id: string): Promise<string> {
    const init = await GoogleDriveApi.getParams();
    const res = await fetch(
      "https://www.googleapis.com/drive/v3/files/" +
        encodeURI(id) +
        "?alt=media&supportsAllDrives=true",
      init
    );
    return res.text();
  }

  // 他ユーザも含め、アプリ固定ファイル名のリストを得る。
  static async queryFiles(
    nameMailConv: (src: string) => Promise<string>,
    // ファイル所有者を指定する場合、ファイル所有者のメールアドレスを復号して渡す
    owner?: string
  ): Promise<GoogleDriveFileInf[]> {
    const init = await GoogleDriveApi.getParams();
    let query = "name = '" + GD_FILE_NAME + "'";
    if (owner) {
      query += " and '" + owner + "' in owners";
    }
    const res = await fetch(
      "https://www.googleapis.com/drive/v3/files?fields=files(id,version,modifiedTime,owners,sharedWithMeTime)&orderBy=modifiedTime+desc&q=" +
        encodeURI(query),
      init
    );
    const resJson = await res.json();
    if (resJson.files instanceof Array) {
      const promises = (resJson.files as any[]).map((f: any) =>
        GoogleDriveFileInf.fromObj(f, nameMailConv)
      );
      return Promise.all(promises);
    }
    throw new Error("Invalid json");
  }
}

Googleへの要望

要望の背景

この様に、簡単に共有アプリが作れますが、何しろ審査が面倒です。しかも、審査がさらに厳しくなる兆候があります。Google APIは審査の厳しさで以下の3ランクに分かれていて、以下の通り真ん中の厳しさから、一番厳しいランクに変更される可能性が高いという事です。

  • Non-sensitive
    • 審査不要
    • Google Driveで言えば、アプリ専用ファイルへのアクセスならこの範囲
      • ユーザファイルとは完全に別扱いで、ユーザファイルにアクセス不可、ユーザもこのファイルにアクセス不可
      • このため、ユーザがGUIで共有設定することはできない
  • Sensitive
    • 審査があり、大変ではあるが、時間と手間をかければ、費用は要求されない
    • 現時点での、Google Drive内のファイルフルアクセス権はこの範囲
  • Restricted
    • 第三者によるセキュリティ審査(有料で十万単位らしい)も必要なので、趣味でやるなら辛い
    • 上記の審査が厳しくなる兆候に、Google Drive内のファイルフルアクセス権がリストアップされている

本手法では、審査不要のアプリ専用ファイルをユーザが確認・共有できれば、フルアクセス権は全く必要ありません。このため、Googleに以下の要望を送りましたが、一人だけより複数人から要望があった方が、Googleも対応してくれる可能性が高いので、本手法を活用し続けるためにご協力をお願いします。

要望の内容

Googleに送る英語バージョン(Google翻訳掛けただけですが)

Would it be possible for the Google Drive API app-only file access scope (https://www.googleapis.com/auth/drive.appdata) to be "viewable and operable by users"?

This brings the following benefits to Google, developers and users:

  • Developer

    • Since the standard GUI of Google Drive can be reused, the barriers to creating shared applications between multiple users are lowered.
  • User

    • Peace of mind because you can always check the recorded contents of the app.
    • It is safe because it is a unified GUI guaranteed by Google Drive, not a file operation UI created by individual developers that you do not know what is going on behind the scenes.
  • Google

    • Google's monitoring responsibility and costs are reduced because the app-specific files are checked by many users. Especially for apps that initially appear safe and then undergo dangerous updates, the initial Google review alone is not enough.

I would appreciate it if you could consider it.

日本語バージョン

Google Drive API のアプリ専用ファイルアクセススコープ( https://www.googleapis.com/auth/drive.appdata )を、「ユーザから閲覧・操作可能」にして頂けないでしょうか?

これにより、Google、開発者、ユーザーに次のメリットがもたらされます。

  • デベロッパー

    • Googleドライブ標準GUIを流用できるため、複数ユーザ間の共有アプリを作成するための障壁を下げる。
  • ユーザー

    • アプリの記録内容をいつでも確認できるので安心。
    • 個々の開発者が作成した、裏で何をしているか分からないファイル操作UIではなく、Googleドライブが保証する統一的なGUIなので安心。
  • Google

    • 多くのユーザの目で、アプリ専用ファイルがチェックされるため、Googleの監視責任・コストが軽減される。特に、最初は安全に見えて、その後危険な更新がされるアプリは、最初のGoogleの審査だけでは限界がある。

ご検討いただければ幸いです。

1
0
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
0