Google Drive API v3を使ってスプレッドシート(csv)や画像(png)のダウンロード・アップロードをやってみた

■ これはなに

  • Drive APIでSpread Sheet(csv)や画像(png)のダウンロード・アップロードが必要で実装に時間がかかった
  • Drive APIの仕様を事前に知っておいた方が実装しやすいので、ブログにアウトプットしておく
  • Drive APIのおかげで、ブラウザからGoogle Driveを開いてポチポチする無駄な手作業は無くなった

■ 前提

■ やったこと

1. APIでアクセスするDriveの範囲(権限)を制限

  • API経由でアクセスできるユーザーをGCPのサービスアカウントで作成する
  • サービスアカウントがアクセスできるDriveの範囲を制限する

手順やコード

  1. GCPの「APIとサービス」→「ライブラリ」でDrive APIを有効にする
  2. GCPの「APIとサービス」→「 認証情報」 →「サービスアカウント」でアカウントを作成
  3. サービスアカウントに設定したメールアドレスで、Driveのフォルダのユーザー権限にサービスアカウントを追加・権限変更ができるようになる
  4. あとは googleapis にサービスアカウントのキー(.json)を下記の方法で渡せば、APIを利用できる
import fs = require('fs');
import { google, drive_v3 } from 'googleapis';
import { JWT } from 'googleapis-common';

export class DriveApp {
  private readonly jwtAuth: JWT;

  /*
   * ref:
   *   - https://developers.google.com/identity/protocols/oauth2/scopes#drive
   */
  constructor() {
    this.jwtAuth = new google.auth.JWT({
      keyFile: '../credentials/service_account_key.json',
      scopes: ['https://www.googleapis.com/auth/drive'],
    });
  }

  private async driveApi(): Promise<drive_v3.Drive> {
    try {
      await this.jwtAuth.authorize();
      return google.drive({
        version: 'v3',
        auth: this.jwtAuth,
      });
    } catch (e) {
      console.log(`🧨 method: driveApi 🧨`);
      throw e;
    }
  }

// 省略

2. DriveのSpread Sheetをcsv形式でダウンロード

注意1: Drive APIはシート指定のダウンロードに未対応

  • Drive APIとは別にGoogle Spread Sheet専用API(Sheets API)もある

  • Drive APIは、Spread Sheet内のシートを指定してダウンロードは出来ない

  • Drive APIは、対象のSpread Sheetの先頭のシートのみダウンロードする

    • Spread Sheet内のシートを指定してデータをダウンロードしたい場合は、Sheets APIなら可能だった
    • しかし、csv形式のダウンロードには未対応で、Sheets APIの独自形式でのダウンロードになる
  • 使うAPIを増やしたくなかったので、Spread Sheetの複数シート管理をやめてファイル単位にシートを分割してDrive APIのみで対応した

注意2:Drive APIの検索クエリは変わってる

  • 世の中には、こういう検索クエリの指定形式もあるのかもしれないが、特殊な仕様でわかりづらかった
  • 特に注意してほしいのは、 name contains '${targetName}' は前方一致であり、部分一致ではないところ

  • 当時の苦悩はTwitterでつぶやいたりした

手順やコード

/*
 * ref:
 *   - https://developers.google.com/drive/api/v3/reference/files/list
 *   - https://developers.google.com/drive/api/v3/reference/files/export
 */
async exportCSV(folderId: string, spreadSheetName: string): Promise<any> {
  try {
    const driveApi = await this.driveApi();
    const response = await driveApi.files.list({
      fields: 'files(id, name)',
      q: DriveApp.queryBuilder(DriveMimeType.spreadsheet, folderId, spreadSheetName),
    });

    const fileId = response.data.files?.[0]?.id;
    if (!fileId) {
      throw new Error(`❌ ファイルが見つかりません(${folderId}, ${spreadSheetName})`);
    }

    const fileExport = await driveApi.files.export({
      fileId: fileId,
      mimeType: 'text/csv',
    });
    return fileExport.data;
  } catch (e) {
    console.log(`🧨 method: exportCSV 🧨`);
    throw e;
  }
}

/*
 * ref:
 *   - ゴミ箱は対象外(trashed = false)
 *   - https://developers.google.com/drive/api/v3/mime-types
 *   - https://developers.google.com/drive/api/v3/reference/query-ref
 */
private static queryBuilder(
  type: DriveMimeType | null,
  folderId: string | null,
  targetName: string | null
): string {
  let query = 'trashed = false';
  query = type ? `mimeType = '${getMimeType(type)}' and` + query : query;
  query = folderId ? `'${folderId}' in parents and` + query : query;
  query = targetName ? `name contains '${targetName}' and` + query : query;
  return query;
}

3. ローカルのcsvをSpread Sheet形式でDriveへアップロード

  • mimeType でアップロード形式を指定できる
  • 間違って別のフォルダに作成したり、ファイルを更新したりしないように、 parentsaddParents にファイルが配置されるフォルダIDを指定する

手順やコード

/*
 * ref:
 *   - https://developers.google.com/drive/api/v3/reference/files/create
 *   - https://developers.google.com/drive/api/v3/manage-uploads#import_to_google_docs_types_
 */
async create(
  folderId: string,
  localFilePath: string,
  spreadSheetName: string
): Promise<drive_v3.Schema$File> {
  try {
    const driveApi = await this.driveApi();
    const response = await driveApi.files.create({
      media: {
        mimeType: 'text/csv',
        body: fs.createReadStream(localFilePath),
      },
      requestBody: {
        parents: [folderId],
        name: spreadSheetName,
        mimeType: getMimeType(DriveMimeType.spreadsheet),
      },
    });
    return response.data;
  } catch (e) {
    console.log(`🧨 method: create 🧨`);
    throw e;
  }
}

/*
 * ref:
 *   - https://developers.google.com/drive/api/v3/reference/files/update
 *   - https://developers.google.com/drive/api/v3/manage-uploads#import_to_google_docs_types
 */
async update(folderId: string, localFilePath: string, fileId: string): Promise<void> {
  try {
    const driveApi = await this.driveApi();
    await driveApi.files.update({
      fileId: fileId,
      addParents: folderId,
      media: {
        mimeType: 'text/csv',
        body: fs.createReadStream(localFilePath),
      },
      requestBody: {
        mimeType: getMimeType(DriveMimeType.spreadsheet),
      },
    });
  } catch (e) {
    console.log(`🧨 method: update 🧨`);
    throw e;
  }
}

4. 画像をpng形式でDriveからダウンロード

手順やコード

/*
 * ref:
 *   - https://developers.google.com/drive/api/v3/reference/files/list
 *   - https://developers.google.com/drive/api/v3/reference/files/get
 *   - https://github.com/googleapis/google-api-nodejs-client/issues/1768
 */
async downloadImage(folderId: string, basePath: string, imageName: string): Promise<void> {
  try {
    const driveApi = await this.driveApi();
    const response = await driveApi.files.list({
      fields: 'files(id, name)',
      q: DriveApp.queryBuilder(null, folderId, imageName),
    });

    const fileId = response.data.files?.[0]?.id;
    if (!fileId) {
      throw new Error(`❌ 画像が見つかりません(${folderId}, ${imageName})`);
    }

    const dest = fs.createWriteStream(`${basePath}/${imageName}`);
    driveApi.files.get(
      {
        fileId: fileId,
        alt: 'media',
      },
      {
        responseType: 'arraybuffer',
      },
      (err, res): void => {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        dest.write(Buffer.from(res.data));
      }
    );
  } catch (e) {
    console.log(`🧨 method: downloadImage 🧨`);
    throw e;
  }
}

■ 終わりに