Gドライブ+GASでcsvと画像ファイル(png)をS3に上げる
はじめに
仕事で、Gドライブにとあるcsvと、画像ファイルを上げると、S3の特定の場所にアップロードをする仕組みを作った。csvだけだったら問題がなかったが、画像も含まれると、まあまあ手間取りが発生したので知見を残しておく。
仕組み
①毎分60秒以内に新しいファイルが上がっていないか確認する
②新しいファイルがGドライブの特定のフォルダ内に上がっていることを確認したらS3にアップロードする
③完了したらSlackに通知する
仕組みの全体図
チームがどんな感じで幸せになるか
・以前は手動でアップロードしていたが、張り付く人が少なくなる
・ 誰でもGドライブにD&DでS3にファイルを上げれるようになる
・上げたことをSlackに通知することで、担当者以外の人も動いていることを確認できるようになる。
使用したライブラリ
名称
GASの「MB4837UymyETXyn8cv3fNXZc9ncYTrHL9」S3
開発者のブログ
Github
基本的な使い方
使い方に関しては、この4ステップで難しくはない。
1. AWSのS3へのアップロード先への権限を絞ったポリシーを付与したユーザーを作成
2.awsAccessKeyId, awsSecretKeyを生成
3. GASの「MB4837UymyETXyn8cv3fNXZc9ncYTrHL9」S3ライブラリをGASに導入
4.ブログにあるように、S3にアップロード
※開発者のブログから、使い方を引用
var s3 = S3.getInstance(awsAccessKeyId, awsSecretKey);
var blob = UrlFetchApp.fetch("http://www.google.com").getBlob();
s3.putObject("bucket", "googlehome", blob, {logRequests:true});
//and to get it back
var fromS3 = s3.getObject("bucket", "googlehome");
ハマったところ
csvだけなら、上記で問題ないのだが、画像の場合上記のライブラリを改造しないと対応できない。
↓これがちょうど同じ問題に遭遇している投稿
このライブラリを使うと、アップロードされたデータは画像ではなく文字列データに変換されてしまう。なので、「S3-for-Google-Apps-Script」のGASライブラリを修正して、ペイロードにバイト配列を使うように修正する必要があった。なおこの通り修正しても、「S3-for-Google-Apps-Script」がアップデートされているため動かないので、こちら↓も参考にする必要があった。
ハマリポイント1
GASでライブラリを改造する方法がわからなかった。
だた、同じファイル内これとこれをコピペして、「getInstance」で読み出せばいいだけだったが、調べ方がなかなかわからずハマってしまった。
var s3 = getInstance( PropertiesService.getScriptProperties().getProperty("AWS_ACCESS_KEY_ID"), PropertiesService.getScriptProperties().getProperty("AWS_SECRET_ACCESS_KEY") );
ハマリポイント2
○リージョンを変更する必要があった
////////////////////////////////////////////
// ここを使うregionに変更
// ref https://qiita.com/daikiichikawa/items/377c95d193e103a9c0d6
////////////////////////////////////////////
//this.region = 'us-east-1';
this.region = 'ap-northeast-1';
ハマリポイント3
今回はcsvの文字列パターンと画像パターンがあるので、条件分岐をさせてどちらにも対応させる必要があった。
ありがとうgithub issue
S3Request.prototype.hexEncodedHash = function(string) {
////////////////////////////////////////////
// ここを修正
////////////////////////////////////////////
//return this.hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, string, Utilities.Charset.UTF_8));
if(typeof string === "string")
return this.hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, string, Utilities.Charset.UTF_8));
else
return this.hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, string.getBytes()));
}
ハマリポイント4
SignatureDoesNotMatchエラーが出た。秘密鍵に「/」が入っていたのが運が悪かった感がある。秘密鍵を再作成して特殊文字が含まれないものを使用して解消。
コード全体
////////////////////////////////////////////////////////////////////////////////////////
// ファイルをGドライブからS3にあげるためのスクリプト
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////
// 説明
////////////////////////////////////////////
// ※使ったS3のライブラリ: MB4837UymyETXyn8cv3fNXZc9ncYTrHL9
// ※S3にあげるためにAWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYが必要です
// ※このGAS用に権限を絞っています
// ※AWS_SECRET_ACCESS_KEYとAWS_SECRET_ACCESS_KEYはユーザープロパティで設定しています。
// ※ここはご自身のGドライブのフォルダ名を入力してください
// 対象のファイルが置かれているフォルダ名、ファイル名を定義
const FOLER_NAME = "アップローダー";
// ※ここはご自身のアップロードしたいファイル名を入力してください
// 対象のファイル名を定義
const FILE_NAMES_MAP = ["hogehoge.csv","fugafuga.png","test.png"]
// ※ここはご自身のアップロードしたいS3のバケット名を入力してください
const S3_BACKET_NAME = "test.assets.test.com"
// ※ここはご自身のアップロードしたいS3のパス名を入力してください
const OUTPUT_PATH_NAME = "test/data/"
// アップロード先のURLを入力してください
const ASSET_URL = "https://test.com/" + OUTPUT_PATH_NAME
// ※ここはご自身のSlackアプリのWebhookを入力してください
// Webhook
var POST_URL = 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
////////////////////////////////////////////
// 新しいファイルが上がっていたらS3に上げるmain関数
////////////////////////////////////////////
function main() {
var date = new Date(); //現在日時のDateオブジェクトを作る
var today = Utilities.formatDate(date, 'JST', 'yyyy/MM/dd HH:mm:ss');
Logger.log(today);
var minutsAgoDate = new Date()
//現在の「秒数」を取得
var todayMinitus = date.getMinutes();
//一分前の秒数にしたいので-1する
minutsAgoDate.setMinutes(todayMinitus-1);
var minutsAgoFormatDate = Utilities.formatDate(minutsAgoDate, 'JST', 'yyyy/MM/dd HH:mm:ss');
Logger.log(minutsAgoFormatDate);
// 指定のファイルを一個ずつ処理する
FILE_NAMES_MAP.map(fileName => {
var fileTimeStamp = getFile(fileName).getLastUpdated();
// ファイルの最終更新が、今の時間の一分前以内だったらS3にアップデートする
if(fileTimeStamp > minutsAgoDate){
// Slackに準備をお知らせ
postMessage("新しい「" + fileName + "」が検知されました! サーバーに上げる準備をします!")
// S3にアップロードする
uploadS3(fileName)
// Slackに完了の通知
postMessage("新しい「" + fileName + "」がサーバーに上がりました!");
}
})
};
////////////////////////////////////////////
// 特定のファイルを取得する
////////////////////////////////////////////
function getFile(fileNamme){
const folders = DriveApp.getFoldersByName(FOLER_NAME);
//フォルダとファイルの検索
while (folders.hasNext()) {
var folder = folders.next();
if (folder.getName() !== FOLER_NAME) { continue; }
var files = folder.getFilesByName(fileNamme);
while (files.hasNext()) {
var file = files.next();
if (file.getName() !== fileNamme) { continue; }
return file;
}
}
}
////////////////////////////////////////////
// Slackに通知
////////////////////////////////////////////
function postMessage(message) {
var jsonData =
{
"text" : message
};
var payload = JSON.stringify(jsonData);
var options =
{
"method" : "post",
"contentType" : "application/json",
"payload" : payload
};
UrlFetchApp.fetch(POST_URL, options);
}
////////////////////////////////////////////
// 新しいファイルが上がっていたらS3に上げる
////////////////////////////////////////////
function uploadS3(fileName){
// S3にアップロードする
// ※AWS_SECRET_ACCESS_KEYとAWS_SECRET_ACCESS_KEYはユーザープロパティで設定しており、それを読み込んでいます。
var s3 = getInstance( PropertiesService.getScriptProperties().getProperty("AWS_ACCESS_KEY_ID"), PropertiesService.getScriptProperties().getProperty("AWS_SECRET_ACCESS_KEY") );
s3.putObject( S3_BACKET_NAME, OUTPUT_PATH_NAME+fileName, getFile(fileName).getBlob(), {logRequests:true} );
}
////////////////////////////////////////////
// S3にあげるライブラリを改造した
// https://github.com/eschultink/S3-for-Google-Apps-Script/issues/8#issuecomment-796907327
////////////////////////////////////////////
/*
* very basic AWS S3 Client library for Google Apps Script
* @author Erik Schultink <erik@engetc.com>
* includes create/delete buckets, create/read/delete objects. very limited support for any optional params.
*
* @see http://engetc.com/projects/amazon-s3-api-binding-for-google-apps-script/
*/
/**
* @license Copyright 2014-15 Eng Etc LLC - All Rights Reserved
*
* LICENSE (Modified BSD) - Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* 1) Redistributions of source code must retain the above copyright notice, this list of conditions and
* the following disclaimer.
* 2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions
* and the following disclaimer in the documentation and/or other materials provided with the
* distribution.
* 3) Neither the name of the Eng Etc LLC, S3-for-Google-Apps-Script, nor the names of its contributors may be used to endorse or
* promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ENG ETC LLC BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
//Body of the library
/* constructs an S3 service
*
* @constructor
* @param {string} accessKeyId your AWS AccessKeyId
* @param {string} secretAccessKey your AWS SecretAccessKey
* @param {Object} options key-value object of options, unused
*
* @return {S3}
*/
function getInstance(accessKeyId, secretAccessKey, options) {
return new S3(accessKeyId, secretAccessKey, options);
}
/* constructs an S3 service
*
* @constructor
* @param {string} accessKeyId your AWS AccessKeyId
* @param {string} secretAccessKey your AWS SecretAccessKey
* @param {Object} options key-value object of options, unused
*/
function S3(accessKeyId, secretAccessKey, options) {
if (typeof accessKeyId !== 'string') throw "Must pass accessKeyId to S3 constructor";
if (typeof secretAccessKey !== 'string') throw "Must pass secretAcessKey to S3 constructor";
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
this.options = options | {};
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
省略
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//wrap object in a Blob if it doesn't appear to be one
if (failedBlobDuckTest) {
object = Utilities.newBlob(JSON.stringify(object), "application/json");
object.setName(objectName);
}
/////////////////////////////////
// 修正箇所
/////////////////////////////////
//request.setContent(object.getDataAsString());
request.setContent(object);
/////////////////////////////////
request.setContentType(object.getContentType());
request.execute(options);
};
////////////////////////////////////////////
// S3にあげるライブラリを改造した
////////////////////////////////////////////
/*
* Most code of AWS Signature Version 4 is ported from the aws-sdk-js
* https://github.com/aws/aws-sdk-js/blob/7cc9ae5b0d7b2935fa69dee945d5f3e6e638c660/lib/signers/v4.js
*
*/
/* constructs an S3Request to an S3 service
*
* @constructor
* @param {S3} service S3 service to which this request will be sent
*/
function S3Request(service) {
this.service = service;
this.httpMethod = "GET";
this.contentType = "";
this.content = ""; //content of the HTTP request
this.bucket = ""; //gets turned into host (bucketName.s3.amazonaws.com)
this.objectName = "";
this.headers = {};
this.date = new Date();
this.serviceName = 's3';
////////////////////////////////////////////
// ここを使うregionに変更
// ref https://qiita.com/daikiichikawa/items/377c95d193e103a9c0d6
////////////////////////////////////////////
//this.region = 'us-east-1';
this.region = 'ap-northeast-1';
this.expiresHeader = 'presigned-expires';
this.extQueryString = '';
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
省略
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/* sets content of request
* @param {string} content request content encoded as a string
* @throws {string} message if invalid input
* @return {S3Request} this request, for chaining
*/
S3Request.prototype.setContent = function(content) {
////////////////////////////////////////////
// ここをコメントアウト
////////////////////////////////////////////
//if (typeof content != 'string') throw 'content must be passed as a string'
this.content = content;
return this;
};
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
省略
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
S3Request.prototype.credentialString = function(datetime) {
return [
datetime.substr(0, 8),
this.region,
this.serviceName,
'aws4_request'
].join('/');
}
S3Request.prototype.hexEncodedHash = function(string) {
////////////////////////////////////////////
// ここを修正
////////////////////////////////////////////
//return this.hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, string, Utilities.Charset.UTF_8));
if(typeof string === "string")
return this.hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, string, Utilities.Charset.UTF_8));
else
return this.hex(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, string.getBytes()));
}
S3Request.prototype.hexEncodedBodyHash = function() {
if (this.isPresigned() && !this.content.length) {
return 'UNSIGNED-PAYLOAD'
} else if (this.headers['X-Amz-Content-Sha256']) {
return this.headers['X-Amz-Content-Sha256']
} else {
return this.hexEncodedHash(this.content || '')
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
省略
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
※↑S3-for-Google-Apps-Scriptを用いて、コードを修正。
全体的にハマって辛かったが、なんとかなってよかったと思った。
今後誰かの参考になれば幸いです・・・・。
参考資料
エンジニアとして働いている成長記録やおもしろいと思ったこと色々書いていこうとおもいます 頂いたご支援は、資料や勉強のための本、次のネタのための資金にし、さらに面白いことを発信するために使います 応援おねがいします