drive.readonly 方便,但是被拒绝了

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

Restricted

但从截图上的 scopes 来看,很明显选择 drive.readonly 是最直接的。确实使用 driver.readonly 开发一路顺畅,直到应用上架的时候,被拒绝了。


Pasted Graphic 1

他们推荐使用 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;
  }

  ... 省略部分代码
}

但是测试结果都是 404 ,如下图:

status 404,

然后同事加入 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) 这个方法的重要性


Pasted Graphic 5

最后

最后的最后,知道问题的原因了,我把这个需要的方法加到正在使用的库了,然后一切都正常工作了。怀疑了一切,都没有怀疑我用的的库,没想到最后被背刺的是我最信任的库~

参考链接

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