■ これはなに
- Drive APIでSpread Sheet(csv)や画像(png)のダウンロード・アップロードが必要で実装に時間がかかった
- Drive APIの仕様を事前に知っておいた方が実装しやすいので、ブログにアウトプットしておく
- Drive APIのおかげで、ブラウザからGoogle Driveを開いてポチポチする無駄な手作業は無くなった
■ 前提
Google Drive API の公式ドキュメントはこちら
npm install googleapis
でインストール、バージョンはv51.0.0
でAPIは最新のv3を利用するNode.jsは
v10.20.1
、 TypeScriptはv3.9.3
です
■ やったこと
1. APIでアクセスするDriveの範囲(権限)を制限
手順やコード
- GCPの「APIとサービス」→「ライブラリ」でDrive APIを有効にする
- GCPの「APIとサービス」→「 認証情報」 →「サービスアカウント」でアカウントを作成
- サービスアカウントに設定したメールアドレスで、Driveのフォルダのユーザー権限にサービスアカウントを追加・権限変更ができるようになる
- あとは
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は、Spread Sheet内のシートを指定してダウンロードは出来ない
Drive APIは、対象のSpread Sheetの先頭のシートのみダウンロードする
使うAPIを増やしたくなかったので、Spread Sheetの複数シート管理をやめてファイル単位にシートを分割してDrive APIのみで対応した
注意2:Drive APIの検索クエリは変わってる
- 世の中には、こういう検索クエリの指定形式もあるのかもしれないが、特殊な仕様でわかりづらかった
特に注意してほしいのは、
name contains '${targetName}'
は前方一致であり、部分一致ではないところ当時の苦悩はTwitterでつぶやいたりした
Google Drive API v3のクエリ指定で文字列のcontainsは部分一致ではなく、前方一致でないとマッチしない。ドキュメントのどこに書いてあるのよ... https://t.co/YBbxEIn45Y
— ストクロ (@kurotyann9696) 2020年5月13日
手順やコード
/* * 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
でアップロード形式を指定できる- 間違って別のフォルダに作成したり、ファイルを更新したりしないように、
parents
やaddParents
にファイルが配置されるフォルダ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からダウンロード
- 一番時間がかかったところだった
- TypeScriptで書く方法がわからず苦戦したが、既にissueになってたので助かった
手順やコード
/* * 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; } }
■ 終わりに
- 苦戦したもののやりたいことは全て実現できた
- こういう作業の自動化はバックオフィス(庶務や総務系)にも需要高そうだけど、既にこれに特化したサービスが世の中にはあるだろうなと思った
- 90日以上ブログを書かないと、はてぶがトップに広告を出すのでその対策もできて良かった