概要
- Google Drive APIを使えば、無料で数十人程度の情報共有アプリを(Google審査以外は)簡単に作れる
- Google Drive標準GUIに乗っかれば、共有関連UIも作成不要
- この記事を参考アプリを作ってGoogleにOAuth申請をする際には、できれば最後に記載の要望を上げてほしい
なぜ簡単に作れるか?
Google Drive上のユーザの手作業で共有関連処理を行えるようにしているからです。以下、自作アプリの共有機能紹介ページからの説明画像の引用です。
共有対象を探して設定してという様な情報をユーザとやり取りするのは面倒ですが、全てGoogle Driveが代わりにやってくれる流れになっています。このため、アプリ固定ファイル名のファイル作成・更新・読み込み・一覧という最低限のAPI利用で、共有アプリに必要な情報交換ができてしまいます。
Google Drive APIとは?
Google Drive内のファイルアクセスをプログラムから自動で行うためのAPIです。ここでリストアップされている様に、Google Driveを使ってユーザができる事はだいたいできます。
ファイルには個人的な共有したくない情報も入っていますが、Google Drive APIは野放しに使われると、その様な情報も含め漏れ放題になってしまいます。
このため、Googleは色々対策をしており、危険な情報にアクセスできるほど厳しいチェックが入ります。残念ながら、今回紹介する簡易共有アプリの作り方だと、Google Driveへのフルアクセスを必要とするため、申請時に厳しいチェックが入ります。(個人や限られたグループ内だけで使うなら、審査不要です。)
以下のGoogle Drive APIコードを見ると分かる通り、アプリ固定ファイル名以外には一切触れない様にはなっています。が、あくまで自主規制なので、悪いことをしようと思えば(そしてGoogleの審査が無ければ)プライバシーダダ洩れにできてしまいます。この懸念に対応するため、共有機能紹介ページでは、以下の様に専用アカウントを作る案内もしています。
Google Drive APIを使ったデータ共有コード
以下、クロム拡張を前提としたコードにはなっていますが、GoogleDriveApi.getParamsだけフレームワークに合わせて取り換えれば、他は流用できるはずです。
// 自分のアプリで同期に使う、他と被りそうにないファイル名
// なお、以下ではファイル内容は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の審査だけでは限界がある。
ご検討いただければ幸いです。