Cloud functionsで画像アップロードの無限列車の旅


あらまし

Cloud functionsでCloud storageにアップロードされたファイルをリサイズして上書きすると無限ループが発生する

詳細

Cloud functionsでstorageのonFinalizeをトリガーにしていると、ファイルのアップロードと更新のたびにこの関数が呼ばれることになります。
で、同じファイル名でファイルをアップロードすると、再度onFinalizeのトリガーが呼ばれて無限に関数が呼ばれるというループに陥るということでした。

問題点1 サンプルコードが不親切

このサンプルコード、ファイル名に「thumb_」とついていたらすでに処理済なので処理しないという判定を入れていますが、それだと同じファイル名でアップロードした場合はファイル名で判定できないため使えません。

Cloud Storage を Cloud Functions で拡張する  |  Firebase

そこでmetadataなるもので印をつけて、判定しようということらしんですが、これにもハマりました。

問題点2 metadataの更新の仕方が深い(ネスト)

単純に

await bucket.upload(<ファイルパス>, {destination: <アップロード先>, metadata: {isConverted: true}});

としただけでは、metadataは更新されません。というか、metadata自体が消えてしまいます。

ここもサンプルコードが意図した動きになっていないとしか言えませんが、metadataの指定は、metadataをmetadataで包む必要があるようです。

ただしくはこちら

await bucket.upload(<ローカルファイルパス>, {destination: <アップロード先>, metadata: {metadata: {isConverted: true}});

参考

ここのサンプルコードは間違っていないように見えるが、実際は間違ってます。気持ち的にはサンプルコードの書き方が自然な気もしますが、理由があるのか、medadataの下にmetadataを設定しないと更新されません

正しそうなサンプルコードはこちらにありました

問題点3 metadataに含まれるfirebaseStorageDownloadTokensは必要

デフォルトでmetadataに設定されているfirebaseStorageDownloadTokensという項目は、消さないほうが良さそうです。というのも、これがなくなるとfirebase consoleでプレビューしたときに画像が表示されません。
ここは謎仕様というか、どこかに説明があってもいいものだと思いますが、試行錯誤の上見つけた内容です。

画像のリサイズなどで、新しくmetadataを設定したい気持ちはありますが、このfirebaseStorageDownloadTokensの値を残すか、埋め込むかをしておいたほうがのちのち楽になりそうな気がしています。
なくても動くので問題ないとは思いますが、firebase consoleで画像が確認できるのは楽ですね。

実際の判定方法

こんな感じで、関数の最初にチェックを入れました。

exports.generateThumbnail = functions.storage.object().onFinalize(async (object) => {
  let metadata = object.metadata
  if (!object.contentType || !object.contentType.startsWith('image/')) {
    return console.log('This is not an image.', object.contentType);
  }
  if (metadata.isConverted) {
    return console.log('Already converted. first.');
  }

また、画像アップロード時のmetadataはもともとのmetadataにisConvertedを設定する方法にしました。firebaseStorageDownloadTokensをなんとなく残したいため

  metadata["isConverted"] = true
  await bucket.upload(<ローカルファイルパス>, {destination: <アップロード先>, metadata: {metadata: metadata}});