Edited at

Cloud Storage for Firebaseにアップロードした画像のサムネイルをCloud Functionsを利用して作成する(Node.js)

Firebase 上のアプリケーションから、Cloud Storageにアップロードした画像を、Cloud Functionsを使ってリサイズ処理を行うサンプルが公式ドキュメントに記載されていますので、一旦そのとおりにやってみました。

https://firebase.google.com/docs/functions/gcp-storage-events

ただし、公式ドキュメントの通りにやるとまず動きません。(※2018年12月5日現在)

どこを変えるとサンプルを実行できるかについて書いておきたいと思います。

いずれドキュメントの内容は更新されると思う(思いたい)ので、この投稿は将来ゴミになるとは思いますが、現時点でハマっている人の少しでも役に立てば幸いです。


最終的に動いたコード

とりあえず結論から。(Node6でも8でもどちらでも可)

ざっくり説明するとこのような流れになっています。


  1. Cloud Storageにファイルがアップロードされたタイミング(onFinalize)で動作

  2. contentTypeが画像系かつ、サムネイル用のプリフィックスが付いていないファイル名なら実行

  3. 一時ディレクトリに該当ファイルをダウンロード

  4. Cloud Functionsに予め用意されている ImageMagick ライブラリで指定サイズに変換

  5. Storageの元画像と同じ場所に、サムネイル用ファイル名でアップロードして戻す


index.js

// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

const {Storage} = require('@google-cloud/storage');
const spawn = require('child-process-promise').spawn;
const path = require('path');
const os = require('os');
const fs = require('fs');

// Download file from bucket.
exports.generateThumbnail = functions.storage.object().onFinalize((object) => {

const gcs = new Storage();
const fileBucket = object.bucket; // The Storage bucket that contains the file.
const filePath = object.name; // File path in the bucket.
const contentType = object.contentType; // File content type.

// [START stopConditions]
// Exit if this is triggered on a file that is not an image.
if (!contentType.startsWith('image/')) {
return console.log('This is not an image.');
}

// Get the file name.
const fileName = path.basename(filePath);
// Exit if the image is already a thumbnail.
if (fileName.startsWith('thumb_')) {
console.log('Already a Thumbnail.');
return null;
}

const bucket = gcs.bucket(fileBucket);
const tempFilePath = path.join(os.tmpdir(), fileName);
const metadata = {
contentType: contentType,
};
return bucket.file(filePath).download({
destination: tempFilePath,
}).then(() => {
console.log('Image downloaded locally to', tempFilePath);
// Generate a thumbnail using ImageMagick.
return spawn('convert', [tempFilePath, '-thumbnail', '200x200>', tempFilePath]);
}).then(() => {
console.log('Thumbnail created at', tempFilePath);
// We add a 'thumb_' prefix to thumbnails file name. That's where we'll upload the thumbnail.
const thumbFileName = `thumb_${fileName}`;
const thumbFilePath = path.join(path.dirname(filePath), thumbFileName);
// Uploading the thumbnail.
return bucket.upload(tempFilePath, {
destination: thumbFilePath,
metadata: metadata,
});
// Once the thumbnail has been uploaded delete the local file to free up disk space.
}).then(() => fs.unlinkSync(tempFilePath));
});



公式ドキュメントの内容との比較

https://firebase.google.com/docs/functions/gcp-storage-events

上記のURLから該当ページを見ると、 index.js にリンクされているサンプルコードがいくつか表示されていて、以下のGitHubのソースコードが表示されます。(ページ内のサンプルコードは全てこちらにリンクされています)

https://github.com/firebase/functions-samples/blob/284835ba9d6662ff3d6242301e018687afda8e9a/quickstarts/thumbnails/functions/index.js

はい、公式ドキュメントのサンプルコードとリンク先のソースコードがまるで別物です💩

(ドキュメントに記載するサンプルコードには、諸々のバージョン情報も記載して欲しいものです…)


修正ポイント


index.js

// 公式ドキュメントのサンプル

const gcs = require('@google-cloud/storage')();

// このように変更
const {Storage} = require('@google-cloud/storage');
// 〜中略〜
const gcs = new Storage();


今回ではハマるポイントはここです。

おそらく require(...) is not a function というエラーが出てしまうことでしょう。こちらですが、GCS Node.js client APIが v2.0.0 になったタイミングで breaking changes が入りました。こちらのリリースノートに詳細が記載されています。

https://github.com/googleapis/nodejs-storage/releases/tag/v2.0.0

まぁこれ自体は、npmパッケージの中の @google-cloud/storage/build/src/index.d.ts を参照すれば、コメントにもこのように書いてあります。

 * @example <caption>Import the client library</caption>

* const {Storage} = require('@google-cloud/storage');
*
* @example <caption>Create a client that uses <a
* href="https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application">Application
* Default Credentials (ADC)</a>:</caption> const storage = new Storage();


GitHub上のサンプルも注意

GitHubのサンプルコードのほうは、そもそもGCSのAPIをrequireしていませんでした。更に微妙な違いとしては、async/await で書かれていました。一応こちらもcloneして試してみたものの、スンナリとは動いてくれませんでした。(気力があったらいずれ書きます)

それ以外の、GitHubにあがっている functions-sample は、 API が古いバージョンのものが多いように見えましたので、参考にする際には真っ先に package.json に記載されているバージョンを確認し、古い場合には自力で変換し、それでも動かない場合は、リリースノート等を確認してみると良いかと思います。

現場からは以上です。