Firebaseのfunctionsでadmin.storage().bucketを使って画像をアップロードした時に困ったこと集

## こんな書き方はできない（firebase client sdkと違う）

まずはじめに、firebase admin sdkでは、
firebaseのフロント側で使うSDKの様にcloud storageをそのまま対応してないみたいです。

そのため、下記のようなコードは書けません。

```ts
import firebase from 'firebase/app'
const storage = firebase.storage()

const uploadImage = async (
  imageData: Blob,
  uploadPath: string
) => {
  const refStorage = storage.ref(uploadPath)

  await refStorage.put(imageData)
  await refStorage.getDownloadURL()
    .then((downloadURL) => {
      console.log('cloud storageに登録した画像URL', downloadURL)
    })
}
```

## functionsで書き込みできるのは`/tmp`のみ

画像データをBufferなどで取得した後にファイルに一時保存したいけど書き込み権限エラーが出る。
`const fs = require('fs')`のファイルシステムなどを使って書き込もうとしますができません。

>ファイル システムで書き込み可能な部分は /tmp ディレクトリだけです。このディレクトリは、関数インスタンスの一時ファイルの保存先として使用できます。このディレクトリは、ボリュームに書き込まれたデータがメモリに格納される「tmpfs」ボリュームと呼ばれるローカル ディスクのマウント ポイントです。これにより、関数用にプロビジョニングされたメモリリソースが消費されるので注意してください。

>ファイル システムの残りの部分は読み取り専用で、関数からアクセスできます。

引用元
https://cloud.google.com/functions/docs/concepts/exec?hl=ja#file_system

### 解決策

cloud functionsで書き込む事が許されているディレクトリの`/tmp`に対して
node.jsの`fs`等を用いて書き込みましょう。

※ `fs`や`os`や`path`が分からなければ **「node.js fs」** などで調べて見てください。

```ts
const fs = require('fs')
const os = require('os')
const path = require('path')
const tempDir: string = os.tmpdir()

const writeImageFile = async (
  imageBuffer: Buffer,
  fileName: string
) => {
  // functionsの/tmpの書き込み先ファイルパスを生成
  const localPath: string = path.join(tempDir, fileName)

  // ローカルの/tmp以下のファイルにBufferの画像データを書き込み
  await fs.writeFile(localPath, imageBuffer, (err: Error) => {
    if (err) { throw err }
  })

}
```

## 画像のリサイズや整形をしたかった

sharp 公式ドキュメント
https://sharp.pixelplumbing.com/

今回はsharpがnode.js上で画像整形するのに都合が良かったので使わせてもらいました！

### toBuffer

Buffer or ファイル形式を受け取ってリサイズする。
`image`の引数には`Buffer`データ以外にも`image.jpg`のような指定ファイルでも使えます！

```ts
const sharp = require('sharp')

export const resizeImageToBuffer = async (
  image: Buffer|string
): Promise<Buffer> => {
  const resizeBuffer: Buffer = await sharp(image)
    .resize({ height: 500, width: 500 })
    .toBuffer()
    .catch((err: Error) => {
      throw err
    })
  return resizeBuffer
}

```

### toFile

こちらの例では`toFile()`メソッドで結果をローカルファイルに書き込みする事ができる。
functionsでは先程の`/tmp`のpathを渡してあげたりなどして`fs`を使わずにファイルを書き込むことも可能

返り値の型を`Promise<void>`と書いているが、`toFile`の返り値は画像データでは無いので敢えて返さないコードを書いてみた。
本来なら成功結果のBooleanを返すといいかも。

```ts
const sharp = require('sharp')

export const resizeImageToFile = async (
  image: Buffer|string,
  localFilePath: string
): Promise<void> => {
  await sharp(image)
    .resize({ height: 500, width: 500 })
    .toFile(localFilePath)
    .catch((err: Error) => {
      throw err
    })
}

```

## uploadしたけどfirebaseコンソール画面で画像が読み込まれなかった（クルクルしてた）

重要なのは`firebaseStorageDownloadTokens`オプションに`uuid v4`を指定すること。
これでアクセストークンができて外部でも読み込み可能になる。

### 注意!!

要注意なのは`{metadata: {metadata: {firebaseStorageDownloadTokens: uuid}}}`のように
`metadata`を二重にして指定してあげないと駄目ってところ。

```ts
import * as admin from 'firebase-admin'
import { v4 as uuidv4 } from 'uuid'

// 環境変数の取得
// NOTE: 複数バケットがある場合は読み分けて
const firebaseConfig: string | any = process.env.FIREBASE_CONFIG
const firebaseConfigObj: object | any = JSON.parse(firebaseConfig)
const bucket = admin.storage().bucket(firebaseConfigObj.storageBucket

export const upload = async (
  localPath: string,
  remotePath: string
) => {
  const uuid = uuidv4()

  await bucket
    .upload(localPath, {
      destination: remotePath,
      metadata: {
        metadata: {
          // uuidv4をトークンに指定すると画像が外部で表示できる
          firebaseStorageDownloadTokens: uuid
        }
      }
    })
}

```

## getDownloadURL()みたいな無期限の保存先URLを取りたい

### No!! getSignedUrl()
`file.getSignedUrl()`で最大7日間の期限付きのURLは取れたけど、
コレではDBに保存しておくタイプの用途には向かない…。

### アクセストークン付きのURLを生成しよう

先程`uuid`のアクセストークンを付けて上げたのが大切
それから`${STORAGE_ROOT}/${bucket.name}/o/${dlPath}?alt=media&token=${uuid}`
`.then()`の引数を使って`DownloadURL`もどきを生成してあげましょう。

また、`.finally()`で登録処理の成否に関わらず`fs.unlinkSync(localPath)`で
書き込んだ画像ファイルを削除してあげましょう。

参考
https://stackoverflow.com/questions/42956250/get-download-url-from-file-uploaded-with-cloud-functions-for-firebase

```ts
import * as admin from 'firebase-admin'
import { v4 as uuidv4 } from 'uuid'
// storageURLの接頭文字列
const STORAGE_ROOT = 'https://firebasestorage.googleapis.com/v0/b'

// 環境変数の取得
// NOTE: 複数バケットがある場合は読み分けて
const firebaseConfig: string | any = process.env.FIREBASE_CONFIG
const firebaseConfigObj: object | any = JSON.parse(firebaseConfig)
const bucket = admin.storage().bucket(firebaseConfigObj.storageBucket

export const upload = async (
  localPath: string,
  remotePath: string
) => {
  const uuid = uuidv4()

  const downloadUrl = await bucket
    .upload(localPath, {
      destination: remotePath,
      metadata: {
        metadata: {
          // uuidv4をトークンに指定すると画像が外部で表示できる
          firebaseStorageDownloadTokens: uuid
        }
      }
    })
    .then((data): string => {
      const file = data[0]
      const dlPath = encodeURIComponent(file.name)
      return `${STORAGE_ROOT}/${bucket.name}/o/${dlPath}?alt=media&token=${uuid}`
    })
    .catch((err) => {
      throw new Error(err)
    })
    .finally(() => {
      // ローカルの一時ファイルの削除
      fs.unlinkSync(localPath)
    })
}

```

## 最後に
firebase admin sdkでstorage使う時は下記のリンクから関数の動きを見ておくように！
このリンクはuploadの関数の内部リンクを付けてます。
https://googleapis.dev/nodejs/storage/latest/Bucket.html#upload