iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
自我挑戰組

我獨自開發 - Supabase 打造全端應用系列 第 14

第十四關 - 惡魔城的鑰匙:Supabase Storage

  • 分享至 

  • xImage
  •  

Supabase API 示意圖

在開發應用程式時,檔案儲存是一個不可避免的需求。無論是用戶頭像、產品圖片、文件下載,都需要安全且高效的檔案儲存方案。

什麼是 Supabase Storage?

Supabase Storage 是一個 S3 相容的物件儲存服務,專為現代應用程式設計。它不只是單純的檔案儲存,更提供了完整的檔案管理生態系統,包括權限控制、即時圖片轉換、CDN 加速等功能。

簡單來說

想像 Supabase Storage 就像是一個倉庫管理員。這個管理員不只幫你存放東西(檔案),還會:

  • 分類整理:把檔案放在不同的「儲存桶」(buckets)裡
  • 門禁管制:控制誰可以存取哪些檔案
  • 即時服務:需要什麼尺寸的圖片就即時提供
  • 快速配送:透過 CDN 讓全世界的用戶都能快速取得檔案

惡魔城的鑰匙:透過盒子提供玩家想要的東西

為什麼要使用 Supabase Storage?

傳統方案的挑戰

  • 複雜設定:需要設定 AWS S3、配置權限、處理 CORS 等
  • 安全考量:檔案權限管理複雜,容易出現安全漏洞
  • 效能問題:沒有 CDN 加速,載入速度慢

Supabase Storage 的優勢

  • 開箱即用:幾分鐘內就能開始使用
  • 整合認證:與 Supabase Auth 完美整合,權限管理簡單
  • 自動優化:內建 CDN 和圖片轉換功能

Supabase Storage 的核心特色

1. 快速且可擴展

  • 全球 CDN:檔案透過全球內容分發網路快速傳遞
  • 自動擴展:隨著使用量增長自動調整容量

2. 與 Postgres 深度整合

  • RLS 支援:使用 Row Level Security 控制檔案存取權限
  • 一致性:檔案和資料庫資料保持同步

3. 即時圖片轉換

  • 動態調整:即時改變圖片大小、格式和品質
  • 格式支援:支援 WebP、AVIF 等圖片格式

4. S3 相容性

  • 標準 API:使用熟悉的 S3 API 進行操作
  • 工具支援:支援現有的 S3 工具和函式庫

如何使用 Supabase Storage

第一步:建立儲存桶(Bucket)

儲存桶就像是檔案的分類資料夾,你可以為不同類型的檔案建立不同的儲存桶。

使用 Dashboard 建立

  1. 進入 Storage 頁面

    • 登入 Supabase Dashboard
    • 選擇你的專案
    • 點擊左側選單的「Storage」
  2. 建立新儲存桶

    點擊「New bucket」按鈕
    → 輸入儲存桶名稱(如:avatars、documents、images)
    → 選擇是否為公開儲存桶
    → 點擊「Save」
    

使用 SQL 建立

也可以使用 SQL 來建立儲存桶:

-- 建立公開儲存桶(任何人都可以存取檔案)
INSERT INTO storage.buckets (id, name, public) 
VALUES ('avatars', 'avatars', true);

-- 建立私人儲存桶(需要權限才能存取)
INSERT INTO storage.buckets (id, name, public) 
VALUES ('documents', 'documents', false);

使用 JavaScript 建立

const { data, error } = await supabase.storage.createBucket('avatars')

第二步:了解公開與私人儲存桶的差別

儲存桶分為兩種類型:

公開儲存桶(Public Buckets)

  • 特點:任何人都可以存取檔案,不需要認證
  • 適用場景:網站 Logo、產品圖片、公開文件
  • 存取方式:直接透過 URL 存取
  • 範例 URLhttps://your-project.supabase.co/storage/v1/object/public/avatars/user1.jpg

私人儲存桶(Private Buckets)

  • 特點:需要適當權限才能存取檔案
  • 適用場景:用戶私人文件、敏感資料、付費內容
  • 存取方式:需要透過 API 並提供認證令牌

第三步:設定 RLS 政策

即使是公開儲存桶,也可能需要控制誰可以上傳、修改或刪除檔案。

-- 允許認證用戶上傳頭像
CREATE POLICY "Users can upload their own avatar" ON storage.objects
  FOR INSERT WITH CHECK (
    bucket_id = 'avatars' 
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- 允許認證用戶查看所有頭像
CREATE POLICY "Anyone can view avatars" ON storage.objects
  FOR SELECT USING (bucket_id = 'avatars');

-- 允許用戶更新自己的頭像
CREATE POLICY "Users can update their own avatar" ON storage.objects
  FOR UPDATE USING (
    bucket_id = 'avatars' 
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- 允許用戶刪除自己的頭像
CREATE POLICY "Users can delete their own avatar" ON storage.objects
  FOR DELETE USING (
    bucket_id = 'avatars' 
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

第四步:前端檔案操作

詳細操作 Supabase Storage 的 API ,可以使用上一篇提到的 JavaScript 客戶端函式庫。

完整的客戶端函式庫:
Supabase Client Libraries | Supabase Docs

上傳檔案

import { supabase } from './supabaseClient'

// 上傳用戶頭像
async function uploadAvatar(file, userId) {
  const fileExt = file.name.split('.').pop()
  const fileName = `${userId}/avatar.${fileExt}`
  
  const { data, error } = await supabase.storage
    .from('avatars')
    .upload(fileName, file, {
      cacheControl: '3600',
      upsert: true // 如果檔案已存在則覆蓋
    })
  
  if (error) {
    console.error('上傳失敗:', error)
    return null
  }
  
  return data
}

下載檔案

有多種方式可以取得檔案:

// 方法一:取得公開 URL(適用於公開儲存桶)
function getPublicUrl(bucket, path) {
  const { data } = supabase.storage
    .from(bucket)
    .getPublicUrl(path)
  
  return data.publicUrl
}

// 方法二:取得簽名 URL(適用於私人儲存桶)
async function getSignedUrl(bucket, path, expiresIn = 3600) {
  const { data, error } = await supabase.storage
    .from(bucket)
    .createSignedUrl(path, expiresIn)
  
  if (error) {
    console.error('取得簽名 URL 失敗:', error)
    return null
  }
  
  return data.signedUrl
}

// 方法三:直接下載檔案
async function downloadFile(bucket, path) {
  const { data, error } = await supabase.storage
    .from(bucket)
    .download(path)
  
  if (error) {
    console.error('下載失敗:', error)
    return null
  }
  
  return data
}

列出檔案

// 列出儲存桶中的所有檔案
async function listFiles(bucket, folder = '') {
  const { data, error } = await supabase.storage
    .from(bucket)
    .list(folder, {
      limit: 100,
      offset: 0,
      sortBy: { column: 'name', order: 'asc' }
    })
  
  if (error) {
    console.error('列出檔案失敗:', error)
    return []
  }
  
  return data
}

刪除檔案

// 刪除單一檔案
async function deleteFile(bucket, path) {
  const { error } = await supabase.storage
    .from(bucket)
    .remove([path])
  
  if (error) {
    console.error('刪除失敗:', error)
    return false
  }
  
  return true
}

// 批量刪除檔案
async function deleteFiles(bucket, paths) {
  const { error } = await supabase.storage
    .from(bucket)
    .remove(paths)
  
  if (error) {
    console.error('批量刪除失敗:', error)
    return false
  }
  
  return true
}

第五步:即時圖片轉換

Supabase Storage 提供強大的即時圖片轉換功能,讓你可以動態調整圖片大小和格式:

// 取得不同尺寸的圖片
function getResizedImageUrl(bucket, path, width, height) {
  const { data } = supabase.storage
    .from(bucket)
    .getPublicUrl(path, {
      transform: {
        width: width,
        height: height,
        resize: 'cover', // 或 'contain', 'fill'
        quality: 80
      }
    })
  
  return data.publicUrl
}

// 範例:取得不同尺寸的頭像
const avatarUrl = getResizedImageUrl('avatars', 'user1/avatar.jpg', 150, 150)
const thumbnailUrl = getResizedImageUrl('avatars', 'user1/avatar.jpg', 50, 50)

最佳實踐建議

1. 檔案命名策略

// 好的命名方式:包含用戶 ID 和時間戳
const fileName = `${userId}/${Date.now()}_${originalFileName}`

// 避免的命名方式:可能造成衝突
const fileName = originalFileName

2. 檔案大小限制

// 在前端檢查檔案大小
function validateFileSize(file, maxSizeMB = 5) {
  const maxSize = maxSizeMB * 1024 * 1024
  if (file.size > maxSize) {
    throw new Error(`檔案大小不能超過 ${maxSizeMB}MB`)
  }
}

3. 錯誤處理

async function uploadWithRetry(bucket, path, file, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const { data, error } = await supabase.storage
        .from(bucket)
        .upload(path, file)
      
      if (!error) return data
      
      // 如果是網路錯誤,重試
      if (error.message.includes('network')) {
        await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
        continue
      }
      
      throw error
    } catch (error) {
      if (i === maxRetries - 1) throw error
    }
  }
}

4. 效能優化

// 使用適當的快取設定
const { data, error } = await supabase.storage
  .from('images')
  .upload(fileName, file, {
    cacheControl: '31536000', // 快取一年
    contentType: file.type
  })

小結

Supabase Storage 提供了一個完整且強大的檔案儲存解決方案,從基本的檔案上傳下載,到進階的權限控制和即時圖片轉換,都能輕鬆實現。

... to be continued

有任何想討論歡迎留言,或需要指正的地方請鞭大力一點,歡迎訂閱、按讚加分享,分享給想要提升開發效率的朋友


上一篇
第十三關 - 支配者之手:Supabase Database API
系列文
我獨自開發 - Supabase 打造全端應用14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言