---
# System prepended metadata

title: "\U0001F3C5 Day 35 - Firebase Storage"
tags: [2025 Node.js 後端工程師培訓營]

---

# 🏅 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) |

