Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
9
Help us understand the problem. What are the problem?

posted at

updated at

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

こんな書き方はできない(firebase client sdkと違う)

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

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

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等を用いて書き込みましょう。

fsospathが分からなければ 「node.js fs」 などで調べて見てください。

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のような指定ファイルでも使えます!

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を返すといいかも。

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を二重にして指定してあげないと駄目ってところ。

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
9
Help us understand the problem. What are the problem?