0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Node.js, heroku】Googleドライブの操作(データ取得/更新)

Last updated at Posted at 2022-01-04

1.herokuからGoogleDriveを操作する

現在herokuで運用中のTwitter botがあります。
ツイートに使用する画像やCSVファイルのデータは、元々herokuにデプロイしてfsモジュールで読み込んでいましたが、下記の理由からそれらのデータをGoogleDriveから取得するように変更しました。
・画像・CSVファイルを編集する度にデプロイする必要がある
・HerokuのSlug Size(Slugは実行モジュール的な何か)が増大する

(1)botで行っているGoogleドライブの操作

以下の処理を行っています。
①ファイルIDの取得
②画像データの取得
③SpreadSheetデータの取得
④SpreadSheetデータの更新
⑤Documentデータの取得
⑥Documentデータの削除
⑦Documentデータの更新

(2)bot本体の仕様

以前作成した下記の星座占いbotの仕様を改善する形で実装しています。
[参考]【Node.js+heroku】Twitter星座占いbot作成

(3)設計書・プログラム

最新の設計書・プログラム等は下記のGoogleDriveに格納しています。
(過去の画像データ等が残ってしまっているため、GitHubは非公開設定です)

2.設計

詳しくは設計書参照なのですが、簡単に説明します。
基本的にファイルIDを取得してからなんやかんやします。

(1)画像データを利用する場合

①画像データのファイルIDの取得
②ファイルから画像データを取得
③画像データをツイートに貼付してツイート送信

(2)SpreadSheetデータを利用する場合

①SpreadSheetデータのファイルIDの取得
②ファイルから文字データを取得
③文字データよりツイート内容を編集してツイート送信

(3)SpreadSheetデータを更新する場合

①SpreadSheetデータのファイルIDの取得
②更新データの編集
③SpreadSheetデータの更新

(4)Documentデータを更新する場合(削除→更新)

①DocumentデータのファイルIDの取得
②Documentデータの最終文字位置を取得
③Documentデータの削除(1桁目~②で取得した最終文字位置まで)
④更新データの編集
⑤Documentデータの更新

3.実装

(1)GoogleCloudPlatformのAPI呼出し

詳しくはプログラム参照なのですが、APIを呼び出す関数は本体から以下のソースに切り出しています。
画像データはダウンロードするのではなく、バイナリデータを取得(responseTypeにarraybufferを指定)して(場合によっては編集を加えて)ツイートに使用しています。

gcpFunc.js
"use strict";
//----------external function declaration----------
const async = require('async');
const {google} = require('googleapis');
const subs = require('./subFunc');

//----------instance declaration----------
const google_auth = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/documents'],
});
const docs   = new google.docs({version: 'v1'});
const drive  = new google.drive({version: 'v3'});
const sheets = new google.sheets({version: 'v4'});

//----------GoogleCloudPlatform functions----------
//①ファイルIDの取得
function getFileID(folder_id, search_file_name) {
    return new Promise(async function(resolve, reject) {
        const params = {q: `'${folder_id}' in parents and trashed = false`, auth : google_auth};
        try {
            const res = await drive.files.list(params);
            const files = res.data.files;
            if (files.length) {
                files.map((file, index) => {
                    if (file.name == search_file_name) {
                        resolve(file.id);
                    } else if (files.length == (index + 1)){
                        reject("File not found:" + folder_id + "/" + search_file_name);
                    }
                });
            } else {
                reject("No files found:" + folder_id);
            }
        } catch (error) {
            reject("File ID get error." + subs.editErrMsg(error));
        }
    });
}

//②画像データの取得
function getImageData(file_id) {
    return new Promise(async function(resolve, reject) {
        try {
            const res = await drive.files.get({fileId: file_id, alt: 'media', auth: google_auth}, {responseType: 'arraybuffer'});
            resolve(Buffer.from(res.data));
        } catch (error) {
            reject("Image get error." + subs.editErrMsg(error));
        }
    });
}

//③SpreadSheetデータの取得
function getSheetData(file_id, arg_range, dimension) {
    return new Promise(async function(resolve, reject) {
        try {
            const params = {spreadsheetId: file_id, range: arg_range, majorDimension: dimension, auth: google_auth};
            const res = await sheets.spreadsheets.values.get(params);
            const get_data = res.data.values;
            if (get_data.length) {
                resolve(get_data);
            } else {
                reject("No files found:" + file_id);
            }
        } catch (error) {
            reject("Sheet get error." + subs.editErrMsg(error));
        }
    });
}

//④SpreadSheetデータの更新
function updateSheetData(file_id, arg_range, arg_value) {
    return new Promise(async function(resolve, reject) {
        try {
            const params = {spreadsheetId: file_id, range: arg_range, valueInputOption: "USER_ENTERED", auth: google_auth, 
                            resource: {values: arg_value} };
            await sheets.spreadsheets.values.update(params);
            resolve("Succeeded");
        } catch (error) {
            reject("Sheet update error." + subs.editErrMsg(error));
        }
    });
}

//⑤Documentデータの取得(最終文字位置を取得)
function getDocLastIndex(file_id) {
    return new Promise(async function(resolve, reject) {
        let res_length = '';
        try {
            const res = await docs.documents.get({documentId: file_id, auth: google_auth});
            res_length = res.data.body.content.length;
            resolve(res.data.body.content[res_length - 1].endIndex -1);
        } catch (error) {
            reject("Doc last index get error." + subs.editErrMsg(error));
        }
    });
}

//⑥Documentデータの削除
function deleteDoc(file_id, end_index) {
    return new Promise(async function(resolve, reject) {
        try {
            await docs.documents.batchUpdate({
                documentId: file_id,
                requestBody: {
                    requests: [{deleteContentRange: {'range': {startIndex: 1,endIndex: end_index,}}}]
                },
                auth: google_auth
            });
            resolve("Succeeded");
        } catch (error) {
            reject("Doc delete error." + subs.editErrMsg(error));
        }
    });
}

//⑦Documentデータの更新
function updateDoc(file_id, write_text) {
    return new Promise(async function(resolve, reject) {
        try {
            await docs.documents.batchUpdate({
                documentId: file_id,
                requestBody: {
                    requests: [{insertText: {location: {index: 1}, text: write_text}}]
                },
                auth: google_auth
            });
            resolve("Succeeded");
        } catch (error) {
            reject("Doc update error." + subs.editErrMsg(error));
        }
    });
}

(2)使用するフォルダの共有設定

データ格納先のフォルダは、GCPサービスアカウントからアクセスできるように、GCPサービスアカウントを共有ユーザーに追加しておく必要があります。
①フォルダを選択
image.png

②「共有」を選択
image.png

③GCPサービスアカウントを追加する
image.png

(3)herokuへのAPIキー設定

GCPサービスアカウントキーはJSON形式になっているので、管理画面からそのままベタ貼りだと認証できません。
一工夫が必要です。
若干違う手順で設定しましたが、ほぼ下記のサイトを参考に設定しました。

[参考]Heroku で Google Cloud API の認証を通す方法

①APIキー設定
APIキーはherokuのコンフィグ管理画面から追加しています。
image.png

・GOOGLE_APPLICATION_CREDENTIALS:/app/google-credentials.json
・GOOGLE_CREDENTIALS:GCPサービスアカウントキー(ベタ貼り)
・DOC_FOLDER_ID:GoogleDriveのドキュメントフォルダのフォルダID
・TABLE_FOLDER_ID:GoogleDriveのテーブルフォルダのフォルダID
・IMAGE_FOLDER_ID:GoogleDriveの画像フォルダのフォルダID

②.profileファイルの作成
下記の内容で作成してデプロイします。

.profile
echo ${GOOGLE_CREDENTIALS} > /app/google-credentials.json

③即時反映したい場合
.profileファイルが実行されるのは次回のdyno再起動時なので、即時反映したい場合は下記の手順が必要。
heroku run bash
echo ${GOOGLE_CREDENTIALS} > /app/google-credentials.json

4.参考サイト

(1)GoogleCloudPlatform API設定関連

[参考]Google APIを利用するためのサービスアカウントの設定(認証)

[参考]APIキー、OAuthクライアントID、サービスアカウントキーの違い:Google APIs

[参考(再掲)]Heroku で Google Cloud API の認証を通す方法

(1)GoogleCloudPlatform APIの利用

①画像データの取得

[参考]Node.jsでGoogle Drive上のファイルをダウンロードする (Google Drive API v3)

[参考]Downloading an image from Drive API v3 continuously gives corrupt images. How should I decode the response from the promise?

[参考]バイナリデータの送信と受信 - Web API - MDN

②SpreadSheetの操作

[参考]【Node.js + Sheets API v4】Googleスプレッドシートを読み書きする

[参考]Google Sheets APIでセルの値を読み込む方法

③Documentの操作

[参考]Insert, delete, and move text  |  Google Docs API  |  Google Developers

[参考]Creating a node script that can write to google docs

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?