Supabase Storage:Image Transformations、Resumable 與 S3 | Supabase 完整教學

2026/10/15
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 一共有四種主要管道,各有明確的最佳戰場:

方式動作走的介面最佳使用時機
標準上傳/取用上傳、下載、取 URLREST(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 就在請求當下把原圖裁切/縮放/壓縮成你要的樣子回傳。

關鍵的技術細節有三個:

  1. URL 路徑會變:帶了 transform,URL 從 /object/ 變成 /render/image/,代表這是一次「渲染請求」而非「直接讀原檔」。
  2. 參數就四個常用的:width、height(1–2500 px)、resize(cover / contain / fill)、quality(20–100,預設 80)。另有 format,預設會依瀏覽器自動轉成更小的 WebP,傳 format: 'origin' 可停用。
  3. 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 用它實作大檔上傳
chunkTUS 把檔案切成的固定大小分塊(Supabase 固定 6MB)
S3 相容 API讓 Storage 表現得像標準 S3 的介面,端點 /storage/v1/s3
S3 Access KeysS3 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 開場,下一篇見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →