Supabase Storage:Image Transformations、Resumable 與 S3 | Supabase 完整教學
上一篇我們把 Storage 的「安全」守住了;這一篇要處理「效能與規模」。你會學到用 Image Transformations 在
getPublicUrl/createSignedUrl帶transform即時產縮圖、搭 CDN 快取省頻寬;用 Resumable Uploads(TUS 協定)穩穩上傳 6MB 以上的大檔;再用 S3 相容 API 讓 AWS SDK、rclone、s3cmd這些既有工具直接操作 Supabase Storage。一次講清「三種上傳/取用方式」各自的定位,Storage 段就此收尾。
前言
一句話定義本篇主題:這一篇把 Supabase Storage 的三個「進階存取管道」講完——Image Transformations(即時把一張原圖渲染成各種尺寸)、Resumable Uploads(用 TUS 協定把大檔切塊、可續傳地上傳)、S3 相容 API(讓標準 S3 工具直接操作 Storage)——並教你在什麼情境下用哪一種。
先接住一個關鍵事實:Supabase Storage 不只有一種「進出」方式。 前兩篇你熟悉的 upload() / getPublicUrl() / createSignedUrl() 是最常用、最直覺的一條路,但它不是唯一。當你要「顯示縮圖」「上傳一支 500MB 的影片」「用 rclone 一次同步一萬張圖」時,硬用標準 upload() 會撞牆——縮圖得自己另外存、大檔會上傳失敗、批次同步得寫一堆迴圈。Supabase 為這些場景各準備了一條專用管道,本篇就是幫你把「什麼時候該切換到哪條管道」內化成直覺。
用一個生活類比幫你抓住全貌:Storage 的三種進階管道,就像一間物流中心的三個不同出入口。
- 標準
upload()/getPublicUrl()是「一般收發櫃台」:小包裹(小檔)走這裡最快,取件也直接給你一張取貨單(URL)。 - Image Transformations 是櫃台旁的「現場加工站」:你寄存一張原尺寸海報(原圖),有人要 A4、有人要名片大小,加工站現場裁切給他,你不必事先印一堆不同尺寸囤著。
- Resumable Upload(TUS) 是「大貨專用碼頭」:大型棧板(>6MB 大檔)分批卸貨,卸到一半停電了,來電後從上次那批繼續卸,不用整車重來。
- S3 相容 API 是「標準貨櫃介面」:任何開著標準貨櫃車(AWS SDK、rclone、s3cmd)的司機都能直接對接,不必為了這間物流中心改裝車輛。
抓住「小檔走標準、縮圖走 transform、大檔走 TUS、批次/工具整合走 S3」這四條路的分工,Storage 的效能與規模這一塊就通了。
本篇你會學到:
- Image Transformations:如何在
getPublicUrl/createSignedUrl帶transform: { width, height, resize, quality }即時產縮圖,以及它與 CDN 快取、計費的關係 - Resumable Uploads(TUS 協定):為什麼 6MB 以上的大檔要改用 TUS、
tus-js-client的核心寫法、續傳與進度回報 - S3 相容 API:如何用 AWS SDK(
S3Client)、rclone、s3cmd直接操作 Storage,兩種認證方式的差異 - 決策地圖:一張表釐清「標準 / transform / resumable / S3」四種方式各自的最佳使用時機
- 常見坑:transform 只 Pro 有、未善用快取、大檔用標準 upload 失敗、S3 憑證外洩
核心概念
Storage 的進階存取只要掌握兩張心智圖:一張是「四種上傳/取用方式的分工表」,一張是「Image Transformations 與 CDN 快取如何協作」。
一、四種方式的分工:一張表看懂何時用哪種
把「上傳」和「取用」攤開,Supabase Storage 一共有四種主要管道,各有明確的最佳戰場:
| 方式 | 動作 | 走的介面 | 最佳使用時機 |
|---|---|---|---|
| 標準上傳/取用 | 上傳、下載、取 URL | REST(SDK upload/getPublicUrl) | 一般檔案 < 6MB(頭像、文件、小圖) |
| Image Transformations | 取「轉換後」的圖片 URL | /render/image/(transform 參數) | 需要縮圖、多尺寸、格式最佳化(Pro 方案) |
| Resumable Upload | 上傳大檔 | TUS 協定(/upload/resumable) | 檔案 > 6MB、網路不穩、要進度條 |
| S3 相容 API | 上傳/下載/同步 | S3 相容(/storage/v1/s3) | 批次遷移、既有 S3 工具、後端自動化 |
這張表的重點不是「背下來」,而是理解它們不是互斥、而是分工:同一個專案裡,前端頭像用標準 upload()、顯示時用 transform 產縮圖、使用者傳大影片時切到 TUS、後端跑批次備份時用 S3 API——它們操作的是同一份 Storage,只是走不同的門。
一個實務上很好用的模式,是寫一個依檔案大小自動選路的 smartUpload:
// 依檔案大小自動選擇「標準上傳」或「TUS 大檔上傳」
const LARGE_FILE_THRESHOLD = 6 * 1024 * 1024 // 6MB
async function smartUpload(bucket: string, path: string, file: File) {
if (file.size > LARGE_FILE_THRESHOLD) {
return uploadWithTus(bucket, path, file) // 大檔走 TUS(下方實作)
}
// 小檔走標準 upload,最省事
return supabase.storage.from(bucket).upload(path, file)
}
二、Image Transformations 與 CDN 快取如何協作
Image Transformations 的核心心智模型是「一張原圖,即時渲染出多種版本」。 你上傳一張大尺寸原圖,之後想要 400×400 縮圖、800 寬中圖、低品質預覽圖,都不必事先各存一份——你在取 URL 時帶 transform 參數,Storage 就在請求當下把原圖裁切/縮放/壓縮成你要的樣子回傳。
關鍵的技術細節有三個:
- URL 路徑會變:帶了
transform,URL 從/object/變成/render/image/,代表這是一次「渲染請求」而非「直接讀原檔」。 - 參數就四個常用的:
width、height(1–2500 px)、resize(cover/contain/fill)、quality(20–100,預設 80)。另有format,預設會依瀏覽器自動轉成更小的 WebP,傳format: 'origin'可停用。 - CDN 快取是省錢與加速的關鍵:相同圖片 + 相同轉換參數的結果會被 CDN 快取,之後同一組參數的請求直接命中快取回傳(快、且不重複計費)。這也是為什麼你該用「固定幾組尺寸」而非每次都帶不同參數——固定尺寸讓快取命中率最大化。
resize 三種模式的差別,對應到你熟悉的 CSS object-fit:
resize | 行為 | 類比 |
|---|---|---|
cover | 維持長寬比,裁掉多餘部分填滿指定尺寸 | CSS object-fit: cover |
contain | 維持長寬比,完整容納(可能留白) | CSS object-fit: contain |
fill | 強制拉伸到指定尺寸(不保長寬比) | CSS object-fit: fill |
三、關鍵術語小抄
| 術語 | 意義 |
|---|---|
| Image Transformations | 即時把原圖渲染成指定尺寸/品質/格式的功能(多為 Pro 方案,以官方為準) |
transform | 傳給 getPublicUrl/createSignedUrl/download 的轉換參數物件 |
/render/image/ | 帶 transform 時的 URL 路徑(相對於直接讀檔的 /object/) |
| TUS | 開放的可續傳 HTTP 上傳協定,Supabase 用它實作大檔上傳 |
| chunk | TUS 把檔案切成的固定大小分塊(Supabase 固定 6MB) |
| S3 相容 API | 讓 Storage 表現得像標準 S3 的介面,端點 /storage/v1/s3 |
| S3 Access Keys | S3 API 的伺服器端金鑰,繞過所有 RLS,絕不可外洩 |
| CDN 快取 | 邊緣節點快取,相同圖片+參數命中後不重算、不重複計費 |
實作範例
以下用可直接執行的 TypeScript,依序示範三種進階管道:transform 產縮圖、TUS 大檔上傳、S3 相容存取。假設 supabase 已用 anon key 初始化。
一、Image Transformations:用 transform 產縮圖
最基本的縮圖,是在 getPublicUrl 帶 transform(avatars 為 public bucket):
// public bucket:取 400x400、cover 模式、品質 80 的縮圖 URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user123/profile.jpg', {
transform: {
width: 400,
height: 400,
resize: 'cover', // 維持長寬比、裁切填滿
quality: 80, // 壓縮品質(20-100,預設 80)
},
})
console.log(data.publicUrl)
// .../storage/v1/render/image/public/avatars/user123/profile.jpg?width=400&height=400&resize=cover&quality=80
// 注意路徑是 /render/image/ 而非 /object/
若是 private bucket,改用 createSignedUrl 帶 transform——一次同時做到「限時授權」和「即時縮圖」:
// private bucket:發一條效期 1 小時、且已縮成 300x300 的簽名 URL
const { data, error } = await supabase.storage
.from('private-photos')
.createSignedUrl('user123/photo.jpg', 3600, {
transform: {
width: 300,
height: 300,
resize: 'cover',
},
})
if (error) throw error
console.log(data.signedUrl) // 帶 token,且回傳的是縮圖
實務上最常見的用法,是包一個小工具函式,搭配 srcset 做 Responsive Images——讓瀏覽器依裝置寬度自動挑最適合的尺寸,手機不必下載桌機大圖:
// 用同一張原圖,即時產生多種尺寸的縮圖 URL
function getThumb(path: string, size: number) {
const { data } = supabase.storage
.from('products')
.getPublicUrl(path, {
transform: { width: size, height: size, resize: 'cover', quality: 75 },
})
return data.publicUrl
}
// 在 React 中用 srcset 提供多解析度(固定幾組尺寸 → CDN 快取命中率高)
function ProductImage({ path }: { path: string }) {
const src = getThumb(path, 800)
return (
<img
src={src}
srcSet={`${getThumb(path, 200)} 200w, ${getThumb(path, 400)} 400w, ${src} 800w`}
sizes="(max-width: 400px) 200px, (max-width: 800px) 400px, 800px"
alt="product"
/>
)
}
重點:固定使用 200 / 400 / 800 這幾組尺寸,而不是每次都算不同數字——這樣同一張圖的同一組參數會一直命中 CDN 快取,既快又不重複計費。
二、Resumable Upload:用 TUS 上傳大檔
大檔(>6MB)改用 tus-js-client。核心是建一個 tus.Upload,指向 Storage 的 resumable 端點,帶上 JWT 與 bucket/物件中繼資料,並固定 6MB 的 chunkSize:
import * as tus from 'tus-js-client'
// 上傳大檔(可續傳、可回報進度)
async function uploadWithTus(bucketName: string, objectName: string, file: File) {
// 取得目前登入者的 access token(授權用)
const { data: sessionData } = await supabase.auth.getSession()
const accessToken = sessionData.session?.access_token
return new Promise<string>((resolve, reject) => {
const upload = new tus.Upload(file, {
// resumable 端點;大檔建議改用直接 storage hostname 取得更好效能
endpoint: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/upload/resumable`,
retryDelays: [0, 3000, 5000, 10000, 20000], // 斷線自動重試的間隔
headers: {
authorization: `Bearer ${accessToken}`,
'x-upsert': 'true', // 允許覆寫同路徑
},
uploadDataDuringCreation: true,
removeFingerprintOnSuccess: true, // 完成後清除指紋,之後可重新上傳
chunkSize: 6 * 1024 * 1024, // 固定 6MB(Supabase 要求)
metadata: {
bucketName,
objectName,
contentType: file.type,
cacheControl: '3600',
},
onError: (err) => reject(err),
onProgress: (uploaded, total) => {
// 拿來畫進度條
const pct = ((uploaded / total) * 100).toFixed(1)
console.log(`上傳進度:${pct}%`)
},
onSuccess: () => resolve(upload.url ?? ''),
})
// 關鍵:先檢查有沒有「上次沒傳完」的紀錄,有就從斷點續傳
upload.findPreviousUploads().then((previous) => {
if (previous.length) {
upload.resumeFromPreviousUpload(previous[0])
}
upload.start()
})
})
}
三個一定要記住的點:一、chunkSize 必須固定 6MB,這是 Supabase 的硬性要求,不能自訂;二、findPreviousUploads() + resumeFromPreviousUpload() 這組是「續傳」的靈魂,斷線後靠它從上次成功的 chunk 接續;三、onProgress 讓你能畫進度條,這正是大檔上傳體驗上贏過標準 upload() 的地方。若不想自己寫 UI,可用 @uppy/tus 搭配 Uppy 的 Dashboard,設定同樣是 endpoint 指向 resumable、chunkSize 6MB。
三、S3 相容 API:用 AWS SDK 操作 Storage
Supabase Storage 提供完整的 S3 相容介面,你可以直接拿 @aws-sdk/client-s3 來操作。重點在 endpoint 指向 Supabase 的 S3 端點、forcePathStyle: true,並填入從 Dashboard 產生的 S3 Access Keys:
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
// 只在「伺服器端」建立:S3 Access Keys 會繞過所有 RLS,絕不可放前端
const s3 = new S3Client({
forcePathStyle: true,
region: 'ap-southeast-1', // 你的 Supabase 專案 region
endpoint: 'https://[project_ref].storage.supabase.co/storage/v1/s3',
credentials: {
accessKeyId: process.env.SUPABASE_S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.SUPABASE_S3_SECRET_ACCESS_KEY!,
},
})
// 上傳(像操作一般 S3 bucket 一樣)
await s3.send(new PutObjectCommand({
Bucket: 'documents',
Key: 'reports/q4-2026.pdf',
Body: pdfBuffer,
ContentType: 'application/pdf',
}))
// 下載
const res = await s3.send(new GetObjectCommand({
Bucket: 'documents',
Key: 'reports/q4-2026.pdf',
}))
如果你要的是「尊重 RLS」的 S3 存取(例如讓某個登入使用者透過 S3 介面只能碰自己的檔案),改用 Session Token 認證:用 project ref 當 accessKeyId、anon key 當 secretAccessKey、使用者 JWT 當 sessionToken:
// 尊重 RLS 的 S3 存取:以使用者 JWT 當 sessionToken
const { data: sessionData } = await supabase.auth.getSession()
const jwt = sessionData.session?.access_token
const s3WithRls = new S3Client({
forcePathStyle: true,
region: 'ap-southeast-1',
endpoint: 'https://[project_ref].storage.supabase.co/storage/v1/s3',
credentials: {
accessKeyId: '[project_ref]', // 專案 reference
secretAccessKey: '[anon_key]', // anon key
sessionToken: jwt, // 使用者 JWT → 仍套用 RLS
},
})
而如果你的目的是批次遷移或同步,根本不必寫程式——用 rclone 或 s3cmd 這類 CLI 工具,一行指令就能把一整個資料夾同步進 Storage:
# 用 rclone 設定 Supabase Storage 為一個 S3 remote
rclone config create supabase-storage s3 \
provider=Other \
endpoint=https://[project_ref].storage.supabase.co/storage/v1/s3 \
access_key_id=YOUR_ACCESS_KEY_ID \
secret_access_key=YOUR_SECRET_ACCESS_KEY \
region=ap-southeast-1
# 一行把本地資料夾同步進 bucket(批次遷移的正解)
rclone sync ./local-folder supabase-storage:my-bucket
這正是 S3 相容 API 最大的價值:你既有的 S3 生態工具與知識,完全不必丟掉。
常見錯誤與最佳實踐
坑一:以為 Image Transformations 到處都能用,結果 Free 方案打不出縮圖。
Image Transformations 通常屬於 Pro 方案及以上的付費功能(以官方為準),Free 方案預設沒有。很多人在本地或 Free 專案上開心地帶了 transform,一到正式站才發現縮圖出不來。正確做法:先確認你的方案是否支援,並在 Dashboard 的 Storage 設定裡啟用 Image Transformations;若在 Free 方案又非要縮圖,替代方案是在上傳時於伺服器端(如 Edge Function)先產生縮圖再存進 bucket。無論如何,方案可用範圍請以 Supabase 官方文件為準。
坑二:每次都帶不同的 transform 參數,快取全都 miss、還多花錢。
Image Transformations 的計費是「每次不同參數的請求算一次」,但相同圖片+相同參數若已被 CDN 快取就不重複計費。若你到處隨手帶 width: 397、width: 401 這種浮動數字,等於每次都是新請求、每次都重算、每次都可能計費。正確做法:固定幾組標準尺寸(例如縮圖 200、中圖 400、大圖 800),全站共用這幾組,讓 CDN 快取命中率最大化——又快又省。
坑三:拿標準 upload() 硬傳大檔,結果上傳失敗或體驗很差。
標準 upload() 是一次性 multipart 上傳,檔案越大越容易撞到請求大小上限而失敗,且中途斷線得整包重傳、沒有進度可顯示。使用者傳一支 300MB 影片時,這條路幾乎必翻車。正確做法:超過 6MB 就改走 TUS Resumable Upload(tus-js-client 或 Uppy),它切成 6MB chunk、可續傳、可回報進度,單檔最大到 5GB。建議直接寫一個依 file.size 自動選路的 smartUpload,讓大小檔各走各的門。
坑四:把 S3 Access Keys 放進前端,等於把 Storage 大門鑰匙貼公告欄。
S3 Access Keys 繞過所有 RLS——拿到它的人可以無視任何權限政策,對整個 Storage 為所欲為。一旦寫進前端 bundle 或 commit 進 git,等於全世界都能操作你的檔案。正確做法:S3 Access Keys 只能放在伺服器端(環境變數、Secrets),絕不 NEXT_PUBLIC_、絕不進版本控制;若前端/使用者情境需要 S3 存取,改用 Session Token(使用者 JWT) 認證,那條路會尊重 RLS。金鑰若疑似外洩,立刻在 Dashboard 撤銷並重新產生。
坑五:換了圖卻看到舊縮圖,誤以為 transform 壞了。
覆寫(upsert)同一路徑的圖後,CDN 快取可能最多 60 秒才失效,加上 transform 的快取,你會一時看到舊版縮圖——這不是 bug,是快取正在生效。正確做法:對「會更新」的圖片,優先上傳到新路徑(加時間戳或 UUID,如 avatar-${Date.now()}.jpg),或在 URL 加版本參數 ?v=2 強制重取;把「覆寫同路徑」留給真的不介意短暫延遲的情境。
最佳實踐總結:
- 四種管道各司其職:小檔標準
upload、縮圖transform、大檔 TUS、批次/工具整合走 S3 - transform 用固定幾組尺寸:最大化 CDN 快取命中,既快又不重複計費
- >6MB 一律 TUS:可續傳、有進度、單檔到 5GB;寫
smartUpload自動選路 - S3 Access Keys 只放伺服器端:絕不進前端與 git;前端情境改用 Session Token(尊重 RLS)
- 會更新的圖用新路徑:避開 CDN/transform 快取造成的「舊圖殘影」
- 不確定的細節以官方為準:方案可用範圍、計費費率隨時可能調整,正式決策前查 Supabase 官方文件
小結
這是 Supabase 系列教學的第 026 篇,也是 Storage 段的收尾。上一篇《Storage 權限與存取控制》我們把 RLS 下沉到 storage.objects,讓每個檔案「誰能讀、誰能寫、誰只能碰自己的」都精準受控——那是 Storage 的「安全」。這一篇,我們補上 Storage 的「效能與規模」:用 Image Transformations 即時產縮圖、用 TUS 穩傳大檔、用 S3 相容 API 接軌既有工具。
核心觀念濃縮成一句話:Supabase Storage 有四條進出管道——小檔走標準 upload/getPublicUrl,縮圖在 getPublicUrl/createSignedUrl 帶 transform: { width, height, resize, quality }(走 /render/image/、搭 CDN 快取省錢),大檔(>6MB)走 TUS Resumable Upload(切 6MB chunk、可續傳、有進度),批次與工具整合走 S3 相容 API(記得 S3 Access Keys 只放伺服器端)——依情境選對門,就能兼顧速度、規模與安全。
- Image Transformations:
transform帶width/height/resize/quality,即時產縮圖,固定尺寸最大化 CDN 快取(多為 Pro,以官方為準) - Resumable Upload(TUS):>6MB 大檔的正解,
chunkSize固定 6MB,findPreviousUploads續傳、onProgress做進度條 - S3 相容 API:AWS SDK/
rclone/s3cmd直接操作,Access Keys 繞過 RLS 只放伺服器端,或用 JWT sessionToken 尊重 RLS - 決策地圖:小檔標準、縮圖 transform、大檔 TUS、批次 S3,四者不互斥而是分工
- 避坑:transform 只 Pro 有、快取要靠固定尺寸、大檔別用標準 upload、S3 憑證絕不外洩
到這裡,Supabase Storage 這一整段——從物件儲存入門、權限與存取控制,到本篇的轉換、大檔與 S3——就完整了。你已經能把「檔案」這件事做到又安全、又快、又能規模化。接下來,我們要離開「靜態的檔案」,走進「即時的資料流」:如何讓資料庫的每一次變動,即時推播到前端。下一篇《Realtime 入門與 Postgres Changes》,我們就來看 Supabase 怎麼把 Postgres 的變更,變成前端可以即時訂閱的事件。Storage 收工,Realtime 開場,下一篇見。