[Nodejs]透過Google Cloud Storage輕鬆展示圖片:打造屬於自己的圖庫

Tiny_Murky
16 min readMar 3, 2024

--

https://cloud.google.com/

前言

本篇文章是講述如何在後端是Nodejs 的情況下,透過nodejs-storage套件,已Google Cloud Storage作為圖床,使用本地檔案上傳的方式,產生出可以回傳至前端並鑲嵌於html中的URL。

本文主要是講述Google Cloud Storage如何設定,程式碼的部份我假設您使用 formidablemulter 等會先將前端傳過來的圖片先存在本地端 /tmp資料夾的套件,這樣才可以使用本地檔案上傳圖片。此外程式碼應該各框架都可以使用,但本文是使用nextjs 後端實做。

撰寫本文的原因是在實做公司官網部落格後台的markdown編輯器時,主管開了Google Cloud Platform(GCP)權限讓我練習,於是我變將練習的過程紀錄下來變成本文。

那就讓我們開始巴!

官方文件參考:

Google Cloud Platform操作

1. 新增專案

在使用Cloud Storage之前,我們需要先在Google Cloud Platform建立一個專案(由於這邊已經存在一個專案,畫面可能不太一樣)

點左上角的專案區

然後選擇專案名稱,如果公司有付費可以選擇機構

進入這個畫面就成功了

2. IAM 權限管理

要從nodejs 上傳圖片,我們需要有一個service account(服務帳戶),他可以賦予程式特定幾組GCP的權限,並使用金鑰或其他方式認證。

點選下面的 IAM與管理

點選左邊的服務帳戶 => 建立服務帳戶

給這個帳號一個名字

給這個帳號 Cloud Storage往下的 Storage管理員Storage物件使用者這兩個權限其中一個

Storage管理員: 權限比較大,可以上傳檔案,還可以將檔案存取狀態設定成 Public,如果之後要在儲存用的值區(Bucket)中選擇 精細調整存取權限的話,建議選此(後面會談到此部份)

Storage物件使用者:權限不包含將檔案存取狀態設定成Public,因此在值區(Bucket)中需要統一把整個Bucket的存取權限設定為公開,外部人員才看得到圖片。

接著按下完成(角色只要選其中一個就好)

3. 新建一個值區(Bucket)

接著要建立一個值區(Bucket),它和 Amazon S3很像,都是一個存放檔案的地方。

回到主畫面,點擊左下角 Cloud Storage

進入值區,點擊建立

取一個名字

選一個儲存位置,可以單選台灣,速度很快,如果有減碳需求可以選擇低二氧化碳標示的地區

預設級別有四種,依照多常存取而區分不同的收費,standard的最貴。如果不確定可以選擇 Autoclass,如果有資料不常被存取,可以允許物件切換到 Coldline 和 Archive

設定存取權,先取消禁止公開存取(因為我的檔案是要供外界讀取),並建議選擇精細,我們可以用 google 提供的nodejs-storage逐一修改檔案的公開狀態(需要搭配擁有Storage管理員 角色的Service Account)

下面的資料保護可以自己選擇,然後按下建立

4. 生成Service Account 金鑰

我們需要Service Account的金鑰才能讓 Nodejs 與 Bucket溝通

回到主畫面=>點選 IAM與管理

點選剛剛建立的服務帳戶=>管理金鑰

點選新增金鑰(下面的金要是我的實驗結果)

點選 JSON格式

建立之後會下載一個金鑰如:專案id-xxxxxxxxxxxx.json ,內容如下,接著會在nodejs連接Cloud Storage的時候使用。

{
"type": "service_account",
"project_id": "",
"private_key_id": "",
"private_key": "",
"client_email": "",
"client_id": "",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "",
"universe_domain": "googleapis.com"
}

後端Nodejs實作上傳功能

以下解說參考下兩個google官網

這裡使用直接將本地檔案上傳的方法,因此建議使用如 formidablemulter 等會先將前端傳過來的圖片先存在本地端 /tmp資料夾的套件。我在這邊是使用formidable,但其實你只要知道你想要上傳的圖片的路徑與檔案名稱就可以操作。

先安裝 nodejs-storage 套件

npm install @google-cloud/storage

1. google_setting.ts

建立一個 google_setting.ts檔案

先列出完整程式碼,於後面解說。

// Ref: https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-client-libraries
import { Storage } from "@google-cloud/storage";

export const googleStorage = new Storage({
projectId: process.env.GOOGLE_PROJECT_ID,
credentials: JSON.parse(process.env.GOOGLE_SERVICE_KEY || '{}'),
});

export const googleBucket = googleStorage.bucket(process.env.GOOGLE_STORAGE_BUCKET_NAME || '');

export function uploadGoogleFile(filePath: string, destFileName: string, generationMatchPrecondition: number) {
const options = {
destination: destFileName,
// Optional:
// Set a generation-match precondition to avoid potential race conditions
// and data corruptions. The request to upload is aborted if the object's
// generation number does not match your precondition. For a destination
// object that does not yet exist, set the ifGenerationMatch precondition to 0
// If the destination object already exists in your bucket, set instead a
// generation-match precondition using its generation number.
preconditionOpts: {ifGenerationMatch: generationMatchPrecondition},
};
const url = `https://storage.googleapis.com/${process.env.GOOGLE_STORAGE_BUCKET_NAME ? process.env.GOOGLE_STORAGE_BUCKET_NAME + '/': ''}${destFileName}`;

return async function uploadFile() {
await googleBucket.upload(filePath, options);
await googleBucket.file(destFileName).makePublic()
return url;
}
}

首先先建立 storage object

import { Storage } from "@google-cloud/storage";

export const googleStorage = new Storage({
projectId: process.env.GOOGLE_PROJECT_ID,
credentials: JSON.parse(process.env.GOOGLE_SERVICE_KEY || '{}'),
});

projectId 可以在 主畫面中得到,放在 .env 中可以保護你的id(也可以直接寫在Code裡)

Storage 認證的方法有兩種

  • keyFilename :直接讀取前面下載的金鑰json的存放位置來取得金鑰
  • credential :當程式中有金鑰json可以取用時,可直接傳入

使用 keyFilename認證,需要先把金鑰json檔案放在專案內,然後把路徑傳給Storage

export const googleStorage = new Storage({
projectId: process.env.GOOGLE_PROJECT_ID,
keyFilename:('/path/to/your/keyfile.json')
});

使用credential的方法,可以先將金鑰用 JSON.stringfy() 的方法轉成字串,並手動複製到.env裡面,字串前後可加引號 GOOGLE_SERVICE="{JSON 文字串}" (但是部署之後建議把部署環境內的文字串的引號刪除)


export const googleStorage = new Storage({
projectId: process.env.GOOGLE_PROJECT_ID,
credentials: JSON.parse(process.env.GOOGLE_SERVICE_KEY || '{}'),
});

建立 Bucket

export const googleBucket = googleStorage.bucket(process.env.GOOGLE_STORAGE_BUCKET_NAME || '');

Storage向下建立值區(Bucket),並傳入你想上傳的值區(Bucket)的名稱,名稱可以從Cloud storage中找到,我將名稱存放在 .env裏面

進入Cloud storage,點擊你的Bucket

複製Bucket 名稱,另外可以先建立資料夾(不一定需要建立)

產生上傳用function

這邊我使用閉包寫法,輸入argument如下:

  • filePath :要上傳檔案的本地存放path
  • destFileName :要上傳的檔案位置與名稱,像是我想要上傳到 km 這個資料夾,就寫成 km/檔案名稱 如果直接存放在Bucket的根資料夾, 檔案名稱 就可以了
  • generationMatchPrecondition :處理同時上傳檔案時會發生的race conditions,可以看這裡的說明,不一定要加,如果加了,第一次上傳時設為0就好
export function uploadGoogleFile(filePath: string, destFileName: string, generationMatchPrecondition: number) {
const options = {
destination: destFileName,
preconditionOpts: {ifGenerationMatch: generationMatchPrecondition},
};

const url = `https://storage.googleapis.com/${process.env.GOOGLE_STORAGE_BUCKET_NAME ? process.env.GOOGLE_STORAGE_BUCKET_NAME + '/': ''}${destFileName}`;

return async function uploadFile() {
await googleBucket.upload(filePath, options);
await googleBucket.file(destFileName).makePublic()
return url;
}
}

上傳完成之後,要從外界觀看上傳的檔案的話,Google提供固定的URL如下,因此可以寫死並回傳

const url = `https://storage.googleapis.com/${Bucket 名稱}/${檔案路徑+檔案名稱}`;

最後會產生一個我們真正會使用的function

  • upload :把檔案上傳

makePublic :將檔案設成公開,這裡需要Bucket的公開存取設定為精確才可以使用

  return async function uploadFile() {
await googleBucket.upload(filePath, options);
await googleBucket.file(destFileName).makePublic()
return url;
}

2. google_drive_upload.ts

新增 google_drive_upload.ts

接著實作上傳功能,這邊假定是使用 formidable 傳入一個 type為formidable.file的 file檔(注意並不是node 原生的 File type),但其實只要能夠取得想要上傳檔案的存放位置,就可以使用。

程式碼如下:

  • fileName :我使用 uuid 產生唯一值uuid名稱避免撞名,並使用 mime-types 產生File的副檔名,但這些不是必須,可以隨意給一個名稱
  • storePath :將 fileName 與我想上傳的資料夾名稱結合,如果存放在根目錄,直接使用fileName就好
  • uploadGoogleFile:產生一個可上傳的function, 傳入
    - 本地端檔案位置
    - 要存放到哪裡
    - 0 (在可以確定不會有race conditions時使用)
  • uploadToGoogle:上傳圖片並回傳上傳後的URL,將這個URL傳給前端,再經過下方CORS的設定就可以正常顯示在畫面了
可以試試看用剛剛寫的程式上傳看看,可以看到存取全都是公開
點入可以看到公開連結,但此連結還需要設定CORS才可以在別的網站使用
點入連結呈現如下

CORS 設定

如果剛剛成功將URL產出,你會發現前端網頁無法正常顯示,這是因為Google會限制可以存取他的資源的網站,我們可以用Cloud Shell設定 (注意:這裡的CORS並不是本地專案後端的問題,不需要在你的專案內修改CORS設定)

以下的設定是可以用Google Cloud 指令列介面 (gcloud CLI),但是因為指令比較少,我使用Google線上的Coding 環境 Cloud Shell執行

在主畫面搜尋 Cloud Shell

新增 cors.json檔案,並輸入下方資訊(檔案位置可以隨意,只要cd進該資料夾就可以)

 [
{
"origin": ["*"]
}
]

輸入以下指令

gsutil cors set ./cors.json gs://你的Bucket名稱

這樣就能正確展示檔案了

結語

不曉得大家有沒有成功呢?我目前前後端都用Next可以實做成功。也希望我們下次可以再相見!

--

--