drive.readonly 方便,但是被拒绝了
最近的项目有个需求就是将用户的 Google Drive 的文件导出到我们自己的应用。从 Google Drive 的文档看下载的逻辑并不难,核心就是获取到用户在 Google Drive 选中的文件的 id 和认证的 token 就可以完成文件的下载了。问题这涉及到 Google Drive 的 scope 选择的问题。

但从截图上的 scopes 来看,很明显选择 drive.readonly 是最直接的。确实使用 driver.readonly 开发一路顺畅,直到应用上架的时候,被拒绝了。
他们推荐使用 driver.file 而不是 Restricted Drive API scopes — driver.readonly 所以问题就变成了如何使用 drive.file 去实现下载文件的功能。
基于 drive.file 的 scope 去实现下载 Google Drive 文件
最初写了如下的代码来实现基本的获取用户选择的文件 id 和 token
const config: GoogleDrivePickerConfig = {
allowMultiSelect: true,
scopes: ['https://www.googleapis.com/auth/drive.file'],
};
const createPicker = (options: CreatePickerOptions) => {
const { oauthToken, apiKey, config, setSelectedFiles } = options;
if (!window.google?.picker) {
handleError('Google Picker API is not available');
return;
}
const pickerBuilder = new window.google.picker.PickerBuilder()
.addView(
config.viewId
? window.google.picker.ViewId[config.viewId]
: window.google.picker.ViewId.DOCS,
)
.setOAuthToken(oauthToken)
.setDeveloperKey(apiKey)
.setCallback((data: PickerCallbackData) => {
if (data.action === window.google?.picker?.Action?.PICKED) {
const files = data.docs.map(
(doc): GoogleDriveFile => ({
id: doc.id,
... 省略部分代码
}),
);
setSelectedFiles(files);
}
});
if (config.allowMultiSelect) {
pickerBuilder.enableFeature(
window.google.picker.Feature.MULTISELECT_ENABLED,
);
}
pickerBuilder.build().setVisible(true);
};
然后通过下述的函数去下载文件
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
interface DownloadedGoogleFile {
buffer?: Buffer;
image?: string; // base64 string
originalname?: string;
mimetype: string;
}
export async function GoogleDownloadFile(
token: string,
fileId: string
): Promise<DownloadedGoogleFile | false> {
const authClient = new OAuth2Client();
authClient.credentials.access_token = token;
const drive = google.drive({ version: 'v3', auth: authClient });
const metadataRes = await drive.files.get({
fileId,
fields: 'name, mimeType',
});
if (metadataRes.status !== 200 || !metadataRes.data) {
return false;
}
const { name, mimeType } = metadataRes.data;
if (!mimeType) return false;
let streamRes;
if (mimeType.startsWith('application/vnd.google-apps.')) {
const exportMimeMap: Record<string, string> = {
'application/vnd.google-apps.document': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.google-apps.spreadsheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.google-apps.presentation': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
};
const exportMimeType = exportMimeMap[mimeType];
if (!exportMimeType) {
console.warn(`Unsupported export type for mimeType: ${mimeType}`);
return false;
}
streamRes = await drive.files.export(
{ fileId, mimeType: exportMimeType },
{ responseType: 'stream' }
);
} else {
streamRes = await drive.files.get(
{ fileId, alt: 'media' },
{ responseType: 'stream' }
);
}
if (streamRes.status !== 200 || !streamRes.data) {
return false;
}
... 省略部分代码
}
然后同事加入 Debug 队伍中,最后他翻到了 Google Picker 的 Api,然后使用它的示例代码竟然成功了。
// Create and render a Google Picker object for selecting from Drive.
function createPicker() {
const showPicker = () => {
// TODO(developer): Replace with your API key
const picker = new google.picker.PickerBuilder()
.addView(google.picker.ViewId.DOCS)
.setOAuthToken(accessToken)
.setDeveloperKey('API_KEY')
.setCallback(pickerCallback)
.setAppId(APP_ID)
.build();
picker.setVisible(true);
}
... 省略部分代码
}
经过对比,我们发现正在使用的那个库没有支持 .setAppId(APP_ID)
这个方法,随后我又翻了一下 PickerBuilder 的方法列表,确定 .setAppId(APP_ID)
这个方法的重要性
最后
最后的最后,知道问题的原因了,我把这个需要的方法加到正在使用的库了,然后一切都正常工作了。怀疑了一切,都没有怀疑我用的的库,没想到最后被背刺的是我最信任的库~
参考链接
Google Drive api scope and file access (drive vs drive.files) - Stack Overflow
Choose Google Drive API scopes | Google for Developers
Download and export files | Google Drive | Google for Developers
The Google Picker API | Google Drive | Google for Developers