こんな書き方はできない(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
等を用いて書き込みましょう。
※ fs
やos
やpath
が分からなければ 「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)
で
書き込んだ画像ファイルを削除してあげましょう。
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