# 🏅 Day 35 - Firebase Storage 要將圖片上傳到 Firebase Storage 主要分為 3 個步驟 : 1. 取得 Firebase Admin SDK 2. Firebase 初始化設定 3. Firebase Storage bucket 概念 ### 1. 取得 Firebase Admin SDK 觀看此[文章](https://israynotarray.com/nodejs/20221225/1867465275/)了解如何建立與設定 Firebase 專案(可以先觀看至「取得 Firebase Admin SDK」)。 取得 Firebase Admin SDK 後會下載一個 JSON 檔案,串接 Firebase 需藉由此金鑰我們才可以使用程式碼對我們建立好的 Firebase Storage 進行相關操作(檔案上傳、刪除等)。 ### 2. Firebase 初始化設定 載入需要使用到的 NPM 套件:[dotenv](https://www.npmjs.com/package/dotenv)、[multer](https://github.com/expressjs/multer)、[uuid](https://www.npmjs.com/package/uuid)、[firebase-admin](https://www.npmjs.com/package/firebase-admin) > `dotenv`:在程式碼中運用環境變數 > `multer`:前一天有提到,作為 middleware 處理上傳檔案 > `uuid`:生成唯一值 > `firebase-admin`:使用 Firebase Admin SDK 1. 新增環境變數: 將我們剛取得的 JSON 檔內容新增進 `.env` 中 ``` FIREBASE_TYPE= FIREBASE_PROJECT_ID= FIREBASE_PRIVATE_KEY_ID= FIREBASE_PRIVATE_KEY= FIREBASE_CLIENT_EMAIL= FIREBASE_CLIENT_ID= FIREBASE_AUTH_URI= FIREBASE_TOKEN_URI= FIREBASE_AUTH_PROVIDER_X509_CERT_URL= FIREBASE_CLIENT_X509_CERT_URL= ``` 2. 建立以下檔案: `service/firebase.js` ```javascript= const dotenv = require('dotenv'); dotenv.config({ path: './config.env' }); const admin = require("firebase-admin"); // Firebase Admin SDK 金鑰內容 const config = { type: process.env.FIREBASE_TYPE, project_id: process.env.FIREBASE_PROJECT_ID, private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID, private_key: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), client_email: process.env.FIREBASE_CLIENT_EMAIL, client_id: process.env.FIREBASE_CLIENT_ID, auth_uri: process.env.FIREBASE_AUTH_URI, token_uri: process.env.FIREBASE_TOKEN_URI, auth_provider_X509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL, }; // 初始化 admin.initializeApp({ credential: admin.credential.cert(config), // 設定身份驗證,可授予 Firebase 服務的管理員存取權 storageBucket: `${process.env.FIREBASE_PROJECT_ID}.appspot.com`, // 設定儲存桶位置(要存放在哪個專案,FIREBASE_PROJECT_ID 即為專案名稱) }); module.exports = admin; ``` 到這邊即可完成 firebase 連線設定 ### 3. Firebase Storage bucket 概念 要將圖片上傳至 Firebase Storage,需先建立一個 bucket 物件,以操作 storage 的儲存桶。 ```javascript= const firebaseAdmin = require('../service/firebase'); const bucket = firebaseAdmin.storage().bucket(); ``` 接著可以建立一個 blob 物件,來存放我們要上傳的檔案 ```javascript= const blob = bucket.file('name'); // name: 要存放在儲存桶中的檔案名稱 // 若要將檔案上傳至特定的資料夾,可以設定: // bucket.file('images/...'),這樣就會將圖片存放到 images 資料夾下 // 可使用 uuid 產生檔案名稱唯一值,避免產生重複的檔案名稱(記得需加上副檔名) const { v4: uuidv4 } = require('uuid'); const blob = bucket.file(`${uuidv4()}.png`) ``` 然後可以建立一個可以寫入 blob 的物件(Writable 可寫入流),讓我們可以在儲存桶中建立檔案 ```javascript= const blobStream = blob.createWriteStream(); ``` 可以使用此物件來監聽檔案的上傳狀態,當上傳完成時,會觸發 finish 事件 ```javascript= blobStream.on('finish', () => { res.send('上傳成功'); }); ``` 我們可以在觸發完成事件後取得檔案的網址 `getSignedUrl(config, callback)` ```javascript= // getSignedUrl 第一個參數為 config 用來設定檔案的存取權限 // action、expires 皆為必填 const config = { action: 'read', // 讀取權限 expires: '12-31-2500', // 網址的有效期限 }; // 取得檔案網址 fileUrl blob.getSignedUrl(config, (err, fileUrl) => { res.send({ fileUrl }); }); ``` 最後寫入檔案的 buffer 後關閉 blobStream ```javascript= blobStream.end(file.buffer); ``` ### 參考資源 - [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup?hl=zh-tw&authuser=0) - [Firebase bucket 儲存桶設定](https://firebase.google.com/docs/storage/admin/start?hl=zh-tw) - [bucket.file()](https://cloud.google.com/nodejs/docs/reference/storage/latest/storage/bucket#_google_cloud_storage_Bucket_file_member_1_) - [createWriteStream()](https://cloud.google.com/nodejs/docs/reference/storage/latest/storage/file#_google_cloud_storage_File_createWriteStream_member_1_) - [getSignedUrl()](https://cloud.google.com/nodejs/docs/reference/storage/latest/storage/file#_google_cloud_storage_File_getSignedUrl_member_2_) 題目 --- 1. 觀看上方成功取得 Firebase Admin SDK(JSON 檔) 2. 觀看上方說明練習並根據註解需求,完整以下程式碼,補上 `...` 的部分,實作出上傳檔案功能 ```javascript= const multer = require('multer'); const path = require('path'); const { v4: uuidv4 } = require('uuid'); const firebaseAdmin = require('../service/firebase'); // multer 處理上傳檔案 const upload = multer({ limits: { fileSize: 2 * 1024 * 1024, }, fileFilter (req, file, cb) { const ext = path.extname(file.originalname).toLowerCase(); if (ext !== '.jpg' && ext !== '.png' && ext !== '.jpeg') { cb('檔案格式錯誤,僅限上傳 jpg、jpeg 與 png 格式。'); } cb(null, true); }, }).any(); // 建立一個 bucket 物件,以操作 storage 的儲存桶 const bucket = firebaseAdmin....; // 將 upload() 作為 middleware 處理上傳的檔案, // 若成功上傳再接續執行以下程式碼 router.post('/file', ..., handleErrorAsync(async (req, res, next) => { if (!req.files.length) { return next(appError(400, "尚未上傳檔案", next)); } // 取得上傳的檔案資訊列表裡面的第一個檔案 const file = req.files[0]; // 需將檔案上傳至特定的資料夾 images 下, // 副檔名可以嘗試從 file 變數取出(file.originalname 可以取得檔案的原始名稱) const blob = bucket.file(`...`); // 建立一個可以寫入 blob 的物件(在儲存桶中建立檔案) const blobStream = blob....(); // 監聽上傳狀態,當上傳完成時,會觸發 finish 事件 blobStream.on('finish', () => { // 設定檔案的存取權限 const config = { ... }; // 取得檔案的網址 blob.getSignedUrl(..., (err, fileUrl) => { res.send({ ... }); }); }); // 如果上傳過程中發生錯誤,會觸發 error 事件 blobStream.on('error', (err) => { res.status(500).send('上傳失敗'); }); // 將檔案的 buffer 寫入 blobStream blobStream.end(file.buffer); })); ``` ## 回報流程 將答案寫在 CodePen 並複製 CodePen 連結貼至底下回報就算完成了喔! 解答位置請參考下圖(需打開程式碼的部分觀看) ![](https://i.imgur.com/vftL5i0.png) <!-- 解答: 1. 有正確下載出金鑰的 JSON 檔即可 2. ```javascript= const multer = require('multer'); const path = require('path'); const { v4: uuidv4 } = require('uuid'); const firebaseAdmin = require('../service/firebase'); const upload = multer({ limits: { fileSize: 2 * 1024 * 1024, }, fileFilter (req, file, cb) { const ext = path.extname(file.originalname).toLowerCase(); if (ext !== '.jpg' && ext !== '.png' && ext !== '.jpeg') { cb('檔案格式錯誤,僅限上傳 jpg、jpeg 與 png 格式。'); } cb(null, true); }, }).any(); const bucket = firebaseAdmin.storage().bucket(); router.post('/file', upload, handleErrorAsync(async (req, res, next) => { if (!req.files.length) { return next(appError(400, "尚未上傳檔案", next)); } const file = req.files[0]; // `file.originalname.split('.').pop()` 取得原本檔案的副檔名 const blob = bucket.file(`images/${uuidv4()}.${file.originalname.split('.').pop()}`); const blobStream = blob.createWriteStream(); blobStream.on('finish', () => { // 設定檔案的存取權限 const config = { action: 'read', // 權限 expires: '12-31-2500', // 網址的有效期限 }; // 取得檔案的網址 blob.getSignedUrl(config, (err, fileUrl) => { res.send({ fileUrl }); }); }); blobStream.on('error', (err) => { res.status(500).send('上傳失敗'); }); blobStream.end(file.buffer); })); ``` --> 回報區 --- <!-- 將答案貼至下方表格內,格式: | Discord 暱稱 | [CodePen](連結) | --> | Discord | CodePen / 答案 | |:-------------:|:-----------------:| | hsin yu | [CodePen](https://codepen.io/tina2793778/pen/jEOaRbY) |