# 🏅 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 連結貼至底下回報就算完成了喔!
解答位置請參考下圖(需打開程式碼的部分觀看)

<!-- 解答:
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) |