Cloud Functions for Firebaseで画像を保存/操作する

Cloud Functions for Firebaseで画像を保存したい場合になかなかうまくいかず、QiitaやZenn上での情報も少なかったため、記事を残します。
下記コードはいずれもNode.js(JavaScript)で記載します。

# はじめに
そもそもFirestore, Realtime Database, Cloud SQL等では画像を保存できないため、Cloud Storageに保存して、その場所を各DBに保存します。

# 取得した画像をCloud Storageに保存する

```JS
const admin = require("firebase-admin");

// jpgイメージの場合
const uploadImage = async (Id, image) => {
    const storage = admin.storage();

    const imageBuffer = Buffer.from(image, "binary");

    const fileName = "testfolder/testImage.jpg";
    const file = storage.bucket().file(fileName);

    // ストレージに画像を保存
    await file.save(imageBuffer, {metadata : {contentType : `image/jpeg`}});

    // firestoreに画像の場所を保存
    await admin.firestore().collection("Ids").doc(Id).update({
        "image" : fileName
    });
}
```
firebase-adminのstorage()を利用して、saveするだけです。


# httpsリクエストから画像を受け取り、Cloud Storageに画像を保存する

firebase-functionsのhttps関数を使用して、multipart/form-dataで送信された画像を保存したい場合、busboyメソッドを使用することで保存することができます。


```JS
const functions = require("firebase-functions/v2");
const admin = require("firebase-admin");
const busboy = require("busboy");

exports.image = functions.https.onRequest(async (req, res) => {
    const storage = admin.storage();
    const bucket = storage.bucket();
    
    try {
        let fileName;
        const bb = busboy({ headers: req.headers });

        const busboyPromise = new Promise((resolve, reject) => {
            bb.on("file", (name, stream, info) => {
                fileName = info.filename;
                const distPath = bucket.file(fileName);

                // busboyのストリームを接続し、Cloud Storage上に保存
                stream.pipe(distPath.createWriteStream()).on("close", () => {
                    resolve();
                })
                .on("error", (error) => {
                    console.log("File processed error", error);
                    reject();
                });
            });
            bb.end(req.rawBody);
        });
    
        await busboyPromise;

        //　ファイル名をFirestore上に保存
        const Id = req.query.Id;
        await admin.firestore().collection("Ids").doc(Id).update({
        "image" : fileName
        });
    
        res.status(200).send("image store success!");
    } catch (error) {
        res.status(400).send(error);
    }
});
```
当初はAPIでmultipart/form-data形式で送った画像をsave()メソッドで保存しようとしていたのですが、うまくいかず苦戦していました。。
一応busboyを使用することは[Google Cloudのドキュメント](https://cloud.google.com/functions/docs/samples/functions-http-form-data?hl=ja)にも書いています。なかなか気づけなかったってのが正直な感想ですが。



# Cloud Storageの画像を取得する
fileNameからdownload()するだけです。
```JS
const admin = require("firebase-admin");

const getImage = async (fileName) => {
    const storage = admin.storage();
    const [image] = await storage.bucket().file(fileName).download();
    return image;
}
```


# おまけ：mimeTypeを判定する
buffer型のデータからmimeTypeを判断する関数も載せておきます。
[file-typeというライブラリ]()があるのですが、CommonJSでは使用できず、自作して使用していました。
```JS

exports.checkFileType = (byteArray) => {
    const patterns = {
        jpeg: [0xFF, 0xD8, 0xFF],
        png: [0x89, 0x50, 0x4E, 0x47],
        gif: [0x47, 0x49, 0x46],
        svg: [0x3C, 0x73, 0x76, 0x67], 
        // Add more patterns for other file types if needed
    };

    for (const [format, pattern] of Object.entries(patterns)) {
        let match = true;
        for (let i = 0; i < pattern.length; i++) {
            if (pattern[i] !== byteArray[i]) {
                match = false;
                break;
            }
        }
        if (match) {
            return format;
        }
    }

    return 'Unknown';
}
```


# 参考文献
 - Firebaseでmultipart/form-dataを処理する

https://cloud.google.com/functions/docs/samples/functions-http-form-data?hl=ja

- busboy npm

https://www.npmjs.com/package/busboy

 - Stream Node.js

https://nodejs.org/api/stream.html

 - file-type npm

https://www.npmjs.com/package/file-type