一步一步帶你用 Node.js 上傳檔案到 Firebase Storage

Firebase Storage

前言

之前有分享過「超完整 Express Imgur 套件上傳教學」,所以這一篇就改來介紹 Firebase Storage 的上傳教學,而這一篇會盡可能流程步驟都說明清楚,因為這一塊也是稍微有一點複雜。

畫面可能會隨著 Firebase 的更新而有所不同,但是大致上的流程應該是一樣的。

Firebase 事前準備

不管怎麼樣,有一些流程還是必須先做好才能進入開發,所以前面就會先帶一下 Firebase 的事前準備。

建立 Firebase 專案

首先開啟 Firebase,並且點選右上角的「轉到控制台」

轉到控制台

接著點「新增專案」、「建立專案」

新增專案

當你點了「建立專案」後,就會跳出一個滿版的視窗,這邊就是你要輸入專案名稱的地方,基本上你可以隨便取,但是我這邊就取「Firebase-Express」,接著按下「繼續」

專案名稱

第二步驟是 Firebase 問你要不要開啟 Google Analytics(分析),但這邊我們並不需要,所以就「關閉」並按下「建立專案」

Google Analytics(分析)

建立過程會稍微等一下,你去上個廁所就好了

建立中

準備好後,就按一下「繼續」

新專案已準備就緒

這時候你應該會直接進入到你的專案控制台,到這邊為止你的 Firebase 專案就準備完成了

完成

設定 Firebase Storage

接下來你必須設置 Firebase Storage 的儲存區域,所以請你點一下左側功能列表找到「Storage」

storage

接著點一下畫面「開始使用」

開始使用

當你點了開始使用後,會跳出「設定 Cloud Storage」的視窗,這邊建議選「以正式模式啟動」

以正式模式啟動

接著會需要選取 Storage 的地區,通常預設是 us-central,而這邊我們沒有必要去調整,所以你可以直接按下「完成」

Storage 地區

過程中會稍微等一下…

等待中...

當你看到這個畫面,就代表你的 Storage 已經設定完成了

設定完成

你如果不打算串接後端 API 你也可以直接在這個視窗上傳圖片。

但是如果你想要串接後端 API,那你就必須往後看唷!

取得 Firebase Admin SDK

接著由於我們需要上傳圖片,因此會使用到 Firebase Admin SDK,所以接著就來取得這個 SDK 吧!

首先點一下上方「齒輪」 => 「專案設定」

專案設定

接著再點一下「服務帳號」,你就可以看到 Firebase Admin SDK 的頁面

Firebase Admin SDK

最後你只要按一下「產生新的私密金鑰」(你可以依照你的程式語言生成範例程式碼),就可以取得你的 Firebase Admin SDK 金鑰,接著請把這份 JSON 檔案保存好,後面我們會使用。

  • 請不要隨意將金鑰提供給別人
  • 請不要將這個金鑰放到你的 GitHub 上(如果你真這樣幹,請你把這個儲存庫刪除並重新生成一個新的金鑰吧。)

到目前為止,我們已經準備好了 Firebase 的事前準備,接著就準備進入寫程式的部分吧。

Express + Firebase

接下來就是建立 Express 的部分,這邊我會使用 Express 產生器來建立一個新的專案,如果你還沒有安裝 Express 產生器,建議你可以先安裝一下,這樣可以讓你的開發速度更快

1
npm install -g express-generator

建立 Express 專案

前面有提到我們會使用 Express 產生器來建立一個新的專案,所以這邊我就直接提供我使用的指令給你,讓你快速建立

1
2
3
4
5
express --view=ejs firebase-express

cd firebase-express

npm install

建立 upload 資料夾

接下來請你在 routes 底下建立一個 upload.js 的檔案,並貼入以下程式碼

1
2
3
4
5
6
7
8
9
10
11
const express = require('express');
const router = express.Router();

router.post('/image', upload.single('file'), function (req, res) {
res.send({
imgUrl: 'https://israynotarray.com/'
});
});


module.exports = router;

接著請記得要在 app.js 中引入這個檔案

1
2
3
4
5
const uploadRouter = require('./routes/upload');

// ... 略過

app.use('/upload', uploadRouter);

不出意外的話,這時候你輸入 npm start 是可以透過 http://localhost:3000/upload/image 來測試你的 API 是否正常運作的,而這邊我推薦可以使用 Postman 來測試,因為這樣可以讓你更清楚的看到你的 API 回傳的資料。

安裝相關套件

接下來我們要安裝幾個套件

  • dotenv
    • 這個套件可以讓你在程式中使用環境變數
  • firebase-admin
    • 這個套件可以讓你使用 Firebase 的 Admin SDK
  • multer
    • 這個套件可以讓你處理上傳檔案的相關事務
1
npm install dotenv firebase-admin multer

firebase 除了有提供 firebase-admin 套件之外,還有另一個叫做 firebase 的套件,這兩個套件很常被搞混,因為不清楚用途。

那麼,這兩個套件的差別是什麼呢?

  • firebase-admin:這個套件可以讓你使用 Firebase 的 Admin SDK,而這個套件可以讓你使用 Firebase 的所有功能,包含認證、資料庫、儲存空間等等。
  • firebase:這個套件可以讓你使用 Firebase 的 Client SDK,而這個套件只能讓你使用 Firebase 的部分功能,例如:認證、儲存空間等等。

建立 .env 檔案

接著請你在專案根目錄下建立一個 .env 檔案,內容如下:

1
2
3
4
5
6
7
8
9
10
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=

而這邊都會對應到你在「取得 Firebase Admin SDK」小節時所取得的 JSON 資料,請你將對應的資料填入 .env 檔案中。

接入 Env 資訊的時候,你可以把 .env 檔案加入到 .gitignore 中,這樣就可以避免你的資訊被其他人看到。

接著請你在專案根目錄下建立一個 connection/firebase.js 檔案,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require('dotenv').config();
const admin = require("firebase-admin");

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),
});

module.exports = admin;

接著只需要回到 routes/upload.js 檔案引入 connection/firebase.js 檔案即可

1
2
3
const firebaseAdmin = require('../connection/firebase');

//...略

接下來我們會準備實作一下上傳功能,那麼官方文件有提供以下這一段程式碼

1
2
3
4
5
6
7
8
9
10
11
const { initializeApp, cert } = require('firebase-admin/app');
const { getStorage } = require('firebase-admin/storage');

const serviceAccount = require('./path/to/serviceAccountKey.json');

initializeApp({
credential: cert(serviceAccount),
storageBucket: '<BUCKET_NAME>.appspot.com'
});

const bucket = getStorage().bucket();

那麼你會發現這邊有一個 <BUCKET_NAME>.appspot.com<BUCKET_NAME> 其實就是你在 Firebase 建立專案時所取得的專案名稱,而 .appspot.com 則是固定的,如果你找不到你也可以看看你的 Firebase 專案的網址,例如:https://<BUCKET_NAME>.firebaseapp.com,那麼 <BUCKET_NAME> 就是你的專案名稱

BUCKET_NAME

而剛好 BUCKET_NAME 就是我們 .env 中的 FIREBASE_PROJECT_ID,所以我們可以把 FIREBASE_PROJECT_ID 的值填入 storageBucket,所以這邊打開 connection/firebase.js 檔案,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require('dotenv').config();
const admin = require("firebase-admin");

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),
storageBucket: `${process.env.FIREBASE_PROJECT_ID}.appspot.com`,
});

module.exports = admin;

到目前為止我們才將 Firebase 初始化完成,接著就準備來實現上傳功能吧。

實作上傳功能

接下來我們要來實作上傳功能,首先前面我們有安裝一個套件叫做 multer,而這個套件就是專門拿來幫我們處理檔案的,所以這邊先在 routes/upload.js 加入以下程式碼

1
2
3
4
5
6
7
const multer  = require('multer');

const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
});

上面的程式碼中,我們將檔案的大小限制再 5MB 以內。

接著由於 multer 是一個 Middleware,所以我們可以在 router.post 中加入 upload.single('file'),這邊的 file 就是我們在前端上傳時所使用的 name,所以這邊我們就可以在 req.file 中取得上傳的檔案資訊

1
2
3
4
5
router.post('/image', upload.single('file'), function (req, res) {
const file = req.file;
console.log(file);
res.send('上傳成功');
});

接著你一樣可以先嘗試使用 Postman 戳戳看,看看是否有正確取得上傳的檔案資訊

Postman

接下來我不得不說 Firebase 的文件關於這一塊的描述說明滿不清楚的,所以這一段我花了一點時間看文件,因此接下來我會區分上傳與取得檔案的網址來說明。

上傳檔案

首先我們要先建立 Storage 的 Bucket(儲存桶),所以這邊我們要在 routes/upload.js 中加入以下程式碼

1
const bucket = firebaseAdmin.storage().bucket();

接著我們要將上傳的檔案存到 Storage 中,所以我們這邊要將路由修改成以下,底下我會盡可搭配註解說明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
router.post('/image', upload.single('file'), function (req, res) {
// 取得上傳的檔案資訊
const file = req.file;
// 基於檔案的原始名稱建立一個 blob 物件
const blob = bucket.file(file.originalname);
// 建立一個可以寫入 blob 的物件
const blobStream = blob.createWriteStream()

// 監聽上傳狀態,當上傳完成時,會觸發 finish 事件
blobStream.on('finish', () => {
res.send('上傳成功');
});

// 如果上傳過程中發生錯誤,會觸發 error 事件
blobStream.on('error', (err) => {
res.status(500).send('上傳失敗');
});

// 將檔案的 buffer 寫入 blobStream
blobStream.end(file.buffer);
});

這時候你在嘗試使用 Postman 上傳檔案,上傳成功後,你應該可以在 Firebase 的 Storage 中看到你上傳的檔案。

上傳檔案

取得檔案網址

雖然我們已經將檔案上傳了,但是卻沒有回傳檔案的網址,所以這邊我們要在上傳成功後,回傳檔案的網址,這邊我們要修改上面的程式碼,讓上傳成功後,回傳檔案的網址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
router.post('/image', upload.single('file'), function (req, res) {
const file = req.file;
const blob = bucket.file(file.originalname);
const blobStream = blob.createWriteStream()

blobStream.on('finish', () => {
// 設定檔案的存取權限
const config = {
action: 'read', // 權限
expires: '12-31-2500', // 網址的有效期限
};
// 取得檔案的網址
blob.getSignedUrl(config, (err, imgUrl) => {
res.send({
imgUrl
});
});
});

blobStream.on('error', (err) => {
res.status(500).send('上傳失敗');
});

blobStream.end(file.buffer);
});

由於取得檔案的網址需要設定檔案的存取權限與有效期限,因此我刻意寫成 2500 年,其實這樣子也等同於永久,而且 expires 這個屬性是必填的,不然會出現 Error: The expiration date provided was invalid. 的錯誤。

這時候你再次使用 Postman 上傳檔案,上傳成功後,你應該可以在回傳的資料中看到檔案的網址。

Firebase 的 Storage 其實是使用了 Google Cloud Storage 來實作,所以你會發現在 Firebase 中的管理者區塊只有提到開始使用,而沒有提到如何上傳檔案,因為這部分是由 Google Cloud Storage 來實作的,因此 Firebase 的文件才會有這一句話

You can use the bucket references returned by the Admin SDK in conjunction with the official Google Cloud Storage client libraries to upload, download, and modify content in the buckets associated with your Firebase projects. Note that you do not have to authenticate Google Cloud Storage libraries when using the Firebase Admin SDK. The bucket references returned by the Admin SDK are already authenticated with the credentials used to initialize your Firebase app.

刪除檔案

這邊也順便補充一下如何刪除上傳的檔案程式碼,這邊我也會補上註解

1
2
3
4
5
6
7
8
9
10
11
12
router.delete('/image', function (req, res) {
// 取得檔案名稱
const fileName = req.query.fileName;
// 取得檔案
const blob = bucket.file(fileName);
// 刪除檔案
blob.delete().then(() => {
res.send('刪除成功');
}).catch((err) => {
res.status(500).send('刪除失敗');
});
});

如果圖片是放在資料夾底下的話,那麼就要包含資料夾一起傳給後端,例如 images/截圖 2022-12-24 17.05.53.png,這樣子才能正確的刪除檔案。

取得檔案名稱列表

當然我也會補上取得檔案名稱列表的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.get('/image', function (req, res) {
// 取得檔案列表
bucket.getFiles().then((data) => {
const files = data[0];
const fileList = [];
files.forEach((file) => {
fileList.push(file.name);
});
res.send(fileList);
})
.catch((err) => {
res.status(500).send('取得檔案列表失敗');
});
});

如果是想要包含連結的話,則改成以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
router.get('/image', function (req, res) {
// 取得檔案列表
bucket.getFiles().then((data) => {

return data[0]
}).then((files) => {
const fileList = [];
for (const file of files) {
// 取得檔案的簽署 URL
const fileUrl = await file.getSignedUrl({
action: 'read',
expires: '03-09-2491'
});
fileList.push({
fileName: file.name,
imgUrl: fileUrl
});
}
res.send(fileList);
}).catch((err) => {
res.status(500).send('取得檔案列表失敗');
});
});

這樣就可以取得檔案名稱和連結了。

檔案名稱與連結

指定上傳檔案到特定資料夾

其實我們上傳圖片的時候,都會希望可以分類,但其實這一段是非常簡單的,只需要在前面的程式碼中,將 bucket.file(file.originalname) 改成 bucket.file('images/' + file.originalname),這樣上傳的檔案就會被放到 images 資料夾中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.post('/image', upload.single('file'), function (req, res) {
const file = req.file;
// 指定上傳的資料夾
const blob = bucket.file(`images/${file.originalname}`);
const blobStream = blob.createWriteStream()

blobStream.on('finish', () => {
res.send('上傳成功');
});

blobStream.on('error', (err) => {
res.status(500).send('上傳失敗');
});

blobStream.end(file.buffer);
});

如果資料不存在的話,Firebase 會自動幫你建立資料夾唷!

中途小結

前面我們分別學習了上傳、取得檔案連結、刪除檔案、檔案列表等語法,這些都可以說是我們上傳檔案的基礎,但這一段其實我們缺少了一些地方,例如上傳檔案後應重新命名避免檔名重複以及先壓縮圖片後再上傳到 Firebase,所以後面就讓我們接著補充這些吧。

加碼補充

接著這邊我額外補充一些我們上傳檔案時會需要的功能,例如重新命名檔案、壓縮圖片等等重點

重新命名檔案

重新命名檔案的方式非常簡單,通常檔案名稱會有幾種方式

  • 時間戳記
  • uuid
  • 亂數

而這邊我們就使用 uuid 來重新命名檔案,那什麼是 uuid 呢?uuid 中文名稱是「通用唯一辨識碼(Universally Unique Identifier)」,並且它的重複機率是趨近於零,所以我們可以安心的使用它來重新命名檔案。

在那之前你必須先替專案安裝 uuid 套件

1
npm install uuid

接著再來修改上傳檔案的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { v4: uuidv4 } = require('uuid');

//...略過

router.post('/image', upload.single('file'), function (req, res) {
const file = req.file;
// 指定上傳的資料夾並將檔案重新命名,請記得補上副檔名
const blob = bucket.file(`images/${uuidv4()}.${file.originalname.split('.').pop()}`);
const blobStream = blob.createWriteStream()

blobStream.on('finish', () => {
res.send('上傳成功');
});

blobStream.on('error', (err) => {
res.status(500).send('上傳失敗');
});

blobStream.end(file.buffer);
});

這樣你的檔案上傳時,就會重新命名了

重新命名

壓縮圖片(TinyPNG)

接著我們要來壓縮圖片,壓縮圖片的方式滿多種的,但是這邊我們將會使用 TinyPNG 來壓縮圖片,而 TinyPNG 是一個圖片壓縮服務,它可以將圖片壓縮到原本的 1/3,而且它的壓縮品質也不會有太大的差異,所以我們可以安心的使用它來壓縮圖片。

免費版的 TinyPNG 有一個限制要稍微注意一下,就是每個月只能壓縮 500 張圖片。

一開始你必須先申請一個 TinyPNG 的帳號,請你先到這個頁面申請

TinyPNG

接著你就會收到一封信,裡面會有進入 TinyPNG 的網址

信件

點擊後,基本上你就可以到達 TinyPNG 的後台,裡面會有一組 API Key 先將它複製下來

API Key

接著就是安裝 TinyPNG 的套件

1
npm install tinify

並將 API Key 設定到環境變數

1
TINYPNG_API_KEY=

接下來就是修改上傳檔案的程式碼就可以囉~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const tinify = require("tinify");
tinify.key = process.env.TINYPNG_API_KEY;

//...略過

router.post('/image', upload.single('file'), function (req, res) {
const file = req.file;

// 上傳圖片到 TinyPNG 並壓縮
tinify.fromBuffer(file.buffer).toBuffer(function(err, resultData) {
if (err) throw err;

const blob = bucket.file(`images/${uuidv4()}.${file.originalname.split('.').pop()}`);
const blobStream = blob.createWriteStream()

blobStream.on('finish', () => {
res.send('上傳成功');
});

blobStream.on('error', (err) => {
res.status(500).send('上傳失敗');
});

// 將壓縮後的圖片上傳到 Firebase Storage
blobStream.end(resultData);
});
});

接著你再去看看你的 Firebase Storage,你會發現你的圖片已經被壓縮,底下這兩個都是相同的圖片,一個是未壓縮 21.47kb,另一個是被壓縮後 6.78kb

壓縮前後

TinyPNG 除了可以壓縮圖片之外,也可以調整圖片大小,詳細可以參考官方文件

當然除了老牌的 TinyPNG 之外,也有 Kraken.io 可以使用唷。

結論

這一篇稍微比較長一點,但是我想應該是滿詳細的了,畢竟 Firebase Storage 這一塊我發現在串接上真的滿複雜的,所以就乾脆把整個流程都給它記錄下來,如果你想要這份範例程式碼的話,我也放在 GitHub 上囉~

GitHub

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ