## 第十三堂:Firebase 檔案與空間上傳 * 錄影 * [Firebase Storage 官網文件](https://firebase.google.com/docs/storage/web/start?hl=zh-tw) * [Ray 教練文件](https://israynotarray.com/nodejs/20221225/1867465275/) - API 應用可參考[線稿圖](https://miro.com/app/board/uXjVLbiDml0=/) - API 開發規格請依照 [此 API 文件](https://liberating-turtle-5a2.notion.site/38e4dd6775894e94a8f575f20ca5b867?v=17c6a246851881208205000cacf67220) - db 欄位設計請依照 [此資料庫欄位圖片](https://firebasestorage.googleapis.com/v0/b/hexschool-courses.appspot.com/o/hex-website%2Fnode%2F1739179853094-fitness_5.png?alt=media&token=a65de209-3ae6-4263-bdc0-d4f08638ff20) 定義 ## 看懂這張圖,就知道傳輸邏輯 ![QWtyvrx__3_](https://hackmd.io/_uploads/ry8ewhq5Je.png) ## 常見 QA Q:要欄位+ 檔案一次傳送?還是檔案先上傳?([範例](https://miro.com/app/board/uXjVLbiDml0=/?moveToWidget=3458764602225541418&cot=14)) ## formidable * [formidable 官方文件](https://github.com/node-formidable/formidable) ## 操作步驟 1. 加入 `formidable`、`firebase-admin` NPM,`npm install firebase-admin formidable` 3. 開啟一個 [Firebase](https://firebase.google.com/) 專案,領取 [300美額度](https://console.cloud.google.com/freetrial/signup/tos?utm_source=firebase_blog&utm_medium=referral&utm_id=gcp_free_trial_firebase) 4. 建立 ENV 檔`FIREBASE_STORAGE_BUCKET=`、`FIREBASE_SERVICE_ACCOUNT=`,放入環境變數,並在 config 加上 secret,`cmd + shift + P,輸入 Join Lines 合併成一行` 6. 在 `app.js`,設置一個 `upload.js` router 7. 在 upload.js 設置整合 Firebase 上傳功能 > 額度顯示 :::spoiler ![截圖 2025-02-25 中午12.28.51](https://hackmd.io/_uploads/BJno2pqqyx.png) ::: ## 將內容變成一行的方法 ``` // config/secret.js module.exports = { jwtSecret: process.env.JWT_SECRET, jwtExpiresDay: process.env.JWT_EXPIRES_DAY, firebase: { serviceAccount: JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT), storageBucket: process.env.FIREBASE_STORAGE_BUCKET } } ``` ## postman 模擬傳送 ![截圖 2025-02-25 凌晨12.58.34](https://hackmd.io/_uploads/S1ISrX99kx.png) ## routes >upload.js ```jsx= const express = require('express') const config = require('../config/index') const { dataSource } = require('../db/data-source') const logger = require('../utils/logger')('Upload') const auth = require('../middlewares/auth')({ secret: config.get('secret').jwtSecret, userRepository: dataSource.getRepository('User'), logger }) const formidable = require('formidable') const firebaseAdmin = require('firebase-admin') firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.cert(config.get('secret.firebase.serviceAccount')), storageBucket: config.get('secret.firebase.storageBucket') }) const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB const ALLOWED_FILE_TYPES = { 'image/jpeg': true, 'image/png': true } const bucket = firebaseAdmin.storage().bucket() const router = express.Router() router.post('/', auth, async (req, res, next)=> { try { const form = formidable.formidable({ multiple: false, maxFileSize: MAX_FILE_SIZE, filter: ({ mimetype }) => { return !!ALLOWED_FILE_TYPES[mimetype] } }) const [fields, files] = await form.parse(req) logger.info('files') logger.info(files) logger.info('fields') logger.info(fields) const filePath = files.file[0].filepath const remoteFilePath = `images/${new Date().toISOString()}-${files.file[0].originalFilename}` await bucket.upload(filePath, { destination: remoteFilePath }) const options = { action: 'read', expires: Date.now() + 24 * 60 * 60 * 1000 } const [imageUrl] = await bucket.file(remoteFilePath).getSignedUrl(options) logger.info(imageUrl) res.status(200).json({ status: 'success', data: { image_url: imageUrl } }) } catch (error) { logger.error(error) next(error) } }) module.exports = router ``` --- ## 其他 :::success #### 有參加「2025 Node.js 後端工程師專題班」的同學: 請在 3/2 (日) 23:59 前填寫[專題班分組表單](https://discord.com/channels/801807326054055996/1310780967425671218/1343754281903198278) ٩(๑•̀ω•́๑)۶ (※ 如果你有參加專題班但無法進入連結,請洽詢[六角客服](https://hex.school/qgbMS))