Firebase Cloud Functionsでサムネイル作成

Firebase Cloud Storageへの画像アップロードをトリガーにして、Function内で画像のリサイズ処理を行ってみたいと思います。
このケースは、[公式のサンプル](https://github.com/firebase/functions-samples/tree/master/generate-thumbnail)も用意されており、今回のサンプルでも大部分を参考にさせてもらいました。  
今回のサンプルで、公式サンプルと異なるのは次の点です。

* TypeScriptで実装
* 画像処理に[sharp](https://www.npmjs.com/package/sharp)を使用(公式サンプルはImageMagic)
* 画像のリサイズ処理のみを実装(公式サンプルはdatabaseに画像URIの保存をする処理まで行っている)

Cloud Functionsが動く仮想マシン上ではImageMagicが使えるため、[公式のサンプル](https://github.com/firebase/functions-samples/tree/master/generate-thumbnail)ではImageMagicのコマンドを使って画像のリサイズを行っているようです。

## サンプルコード

```typescript:generateThumbnail.ts
import * as functions from 'firebase-functions';
import * as Storage from '@google-cloud/storage';
import * as sharp from 'sharp';
import * as path from 'path';
import * as os from 'os';

const THUMB_MAX_WIDTH = 200;
const THUMB_PREFIX = 'thumb_';

// sharpで最大幅のみを指定してリサイズ
const resizeImage = (tmpFilePath: string, destFilePath: string, width: number): Promise<any> => {
  return new Promise((resolve, reject) => {
    sharp(tmpFilePath)
      .resize(width, null)
      .toFile(destFilePath, (err, info) => {
        if (!err) {
          resolve();
        } else {
          reject(err);
        }
      });
  });
};

export const generateThumbnail = functions.storage.object().onChange(event => {
  // eventからファイル情報取得
  const filePath = event.data.name;
  const contentType = event.data.contentType;
  const fileDir = path.dirname(filePath);
  const fileName = path.basename(filePath);
  const thumbFilePath = path.normalize(path.join(fileDir, `${THUMB_PREFIX}${fileName}`));
  const tempLocalFile = path.join(os.tmpdir(), filePath);
  const tempLocalThumbFile = path.join(os.tmpdir(), thumbFilePath);

  // 画像以外だったら何もしない
  if (!contentType.startsWith('image/')) {
    console.log('This is not an image.');
    return null;
  }

  // 既にサムネイル画像だったら何もしない
  if (fileName.startsWith(THUMB_PREFIX)) {
    console.log('Already a Thumbnail.');
    return null;
  }

  // 削除時だったら何もしない
  if (event.data.resourceState === 'not_exists') {
    console.log('This is a deletion event.');
    return null;
  }

  const storage: any = Storage();
  const bucket = storage.bucket(event.data.bucket);
  const file = bucket.file(filePath);
  const metadata = { contentType: contentType };

  (async () => {
    // バケットにアップロードされたファイルを仮想マシンのテンプディレクトリにダウンロード
    await file.download({ destination: tempLocalFile });

    // リサイズ
    await resizeImage(tempLocalFile, tempLocalThumbFile, THUMB_MAX_WIDTH);

    // リサイズされたサムネイルをバケットにアップロード
    await bucket.upload(tempLocalThumbFile, { destination: thumbFilePath, metadata: metadata });
  })()
  .then(() => console.log('Generate Thumbnail Success!'))
  .catch(err => console.log(err));
});
```

* functions.storage.object()
    * ここですべてのバケットへの変更に対してのイベントでフックされるFunctionであることを宣言

* resizeImage()
    * 今回使用したsharpのAPIはコールバックで実装されているため、async/awaitで使えるようPromiseでラップ

* (async () => {}の中
    * 画像ダウンロード、リサイズ、アップロードのメイン処理ではasync/awaitを使って同期処理っぽく記述

## 依存関係

package.jsonは次の通りです。
また、このプロジェクトはFirebase CLI(v3.16.0)で初期化しています。

```json:package.json
{
  "name": "functions",
  "scripts": {
    "build": "./node_modules/.bin/tslint -p tslint.json && ./node_modules/.bin/tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase experimental:functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "main": "lib/index.js",
  "dependencies": {
    "@google-cloud/storage": "^1.5.1",
    "firebase": "^4.7.0",
    "firebase-admin": "~5.4.2",
    "firebase-functions": "^0.7.1",
    "sharp": "^0.18.4"
  },
  "devDependencies": {
    "@types/sharp": "^0.17.6",
    "tslint": "^5.8.0",
    "typescript": "^2.5.3"
  },
  "private": true
}
```

※ 今回は使わなかたけど、[@types/google-cloud__storage](https://www.npmjs.com/package/@types/google-cloud__storage)もあるみたい。


## 動作確認

まずは、Firebase CLIでFunctionをデプロイ。

```
$ firebase deploy --only functions
```

コンソールからデプロイしたFunctionが確認できました。

![スクリーンショット_2017-12-28_16_56_16.png](https://qiita-image-store.s3.amazonaws.com/0/79414/2ad055b4-bca1-a16e-b5d7-635b034e6839.png)


コンソールからCloud Storageに直接画像をアップロードすると、しばらくして`thumb_`ファイルが作成作成されます。

![スクリーンショット 2017-12-28 17.11.34.png](https://qiita-image-store.s3.amazonaws.com/0/79414/9d75ff05-3c43-600e-691d-0c38e4e12f51.png)




Functionのログにも実行された記録が残っているかと思います。

## その他

仮想マシン上でディレクトリ操作を行うのが気持ち悪く感じたため、[imagemagick](https://www.npmjs.com/package/imagemagick)や[gm](https://www.npmjs.com/package/gm)などのライブラリを使いBufferで画像を扱う方法を考えましたが、Cloud StorageのアップロードAPIがそれらに対応していなかったので、最終的に公式サンプルと同様にダウンロードしたファイルを一旦ファイルに保存する処理を行っています。
また、 Cloud Storageを扱う際はGoogle Cloud StorageのSDKを使う必要がある点はわかりづらい点でした。
(Firebase　Cloud StorageのSDKではクライアント側からダウンロードURLを発行するといったAPIしか用意されていない)
Cloud Functions、Cloud Storage周りはまだ発展途上だと思うので、今後FirebaseのSDKだけで完結するようになるといいなぁと思いました。
色々イケてない点はありますが、サンプルということで...

## 参考

* [Cloud Storage を Cloud Functions で拡張する -Firebase](https://firebase.google.com/docs/storage/extend-with-functions?hl=ja)
* [Automatically Generate Thumbnails -GitHub](https://github.com/firebase/functions-samples/tree/master/generate-thumbnail)