跳到主要内容

文件存储

Supabase Storage 提供了类似云存储的文件管理功能,支持上传、下载、删除文件,以及图片转换和优化。本章节介绍如何使用 Storage 管理用户文件。

存储概述

Supabase Storage 基于 S3 兼容的对象存储构建,提供以下功能:

  • 创建存储桶组织文件
  • 支持公开和私有访问
  • 内置图片转换和优化
  • 与 RLS 集成实现文件权限控制
  • CDN 加速全球访问

存储桶(Bucket)

存储桶是文件的容器,类似于文件夹。每个存储桶可以设置不同的访问权限:

  • Public:所有人可以访问
  • Private:需要认证才能访问

文件路径

文件在存储桶中以路径组织:

bucket-name/
├── avatars/
│ ├── user1.jpg
│ └── user2.png
├── documents/
│ ├── report.pdf
│ └── data.xlsx
└── images/
└── photo.jpg

创建存储桶

在 Dashboard 中创建

  1. 进入 Storage 页面
  2. 点击 "Create a new bucket"
  3. 填写桶名称
  4. 选择是否公开访问
  5. 点击 "Create bucket"

使用 SDK 创建

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)

// 创建公开桶
const { data, error } = await supabase.storage.createBucket('avatars', {
public: true,
fileSizeLimit: 1024 * 1024 * 2 // 2MB 限制
})

// 创建私有桶
const { data, error } = await supabase.storage.createBucket('documents', {
public: false
})

使用 SQL 创建

-- 创建存储桶记录
insert into storage.buckets (id, name, public)
values ('avatars', 'avatars', true);

上传文件

基本上传

// 从文件输入上传
const fileInput = document.querySelector('#file-input')
const file = fileInput.files[0]

const { data, error } = await supabase.storage
.from('avatars')
.upload('user1.jpg', file)

if (error) {
console.error('上传失败:', error)
} else {
console.log('上传成功:', data.path)
}

上传到指定路径

// 上传到 avatars 目录
const { data, error } = await supabase.storage
.from('avatars')
.upload('avatars/user123/profile.jpg', file)

// 使用用户 ID 作为路径
const userId = 'user-uuid'
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.jpg`, file)

上传选项

const { data, error } = await supabase.storage
.from('avatars')
.upload('user1.jpg', file, {
cacheControl: '3600', // 缓存时间
upsert: true, // 覆盖已存在的文件
contentType: 'image/jpeg' // MIME 类型
})

上传 Base64 图片

// 从 Base64 上传
const base64Data = 'data:image/png;base64,iVBORw0KGgo...'

const { data, error } = await supabase.storage
.from('avatars')
.upload('avatar.png', base64Data, {
contentType: 'image/png'
})

上传 Blob

// 从 Canvas 获取 Blob
const canvas = document.querySelector('canvas')
canvas.toBlob(async (blob) => {
const { data, error } = await supabase.storage
.from('avatars')
.upload('canvas-image.png', blob)
})

下载文件

获取公开 URL

对于公开桶,可以直接获取 URL:

const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user1.jpg')

console.log(data.publicUrl)
// https://your-project.supabase.co/storage/v1/object/public/avatars/user1.jpg

下载私有文件

对于私有桶,需要生成签名 URL:

// 生成有效期 1 小时的签名 URL
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl('report.pdf', 3600)

if (data) {
console.log(data.signedUrl)
// 该 URL 在 1 小时内有效
}

直接下载文件

// 下载文件为 Blob
const { data, error } = await supabase.storage
.from('documents')
.download('report.pdf')

if (data) {
// 创建下载链接
const url = URL.createObjectURL(data)
const a = document.createElement('a')
a.href = url
a.download = 'report.pdf'
a.click()
URL.revokeObjectURL(url)
}

图片转换

Supabase 支持实时图片转换,无需预先处理图片。

调整尺寸

// 获取缩略图
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user1.jpg', {
transform: {
width: 100,
height: 100,
resize: 'cover' // cover, contain, fill
}
})

console.log(data.publicUrl)

转换格式

// 转换为 WebP 格式
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user1.jpg', {
transform: {
width: 200,
format: 'webp',
quality: 80
}
})

签名 URL 图片转换

const { data } = await supabase.storage
.from('avatars')
.createSignedUrl('user1.jpg', 3600, {
transform: {
width: 300,
height: 300,
resize: 'contain'
}
})

支持的转换选项

选项说明示例
width宽度200
height高度200
resize调整模式cover, contain, fill
format输出格式webp, png, jpg
quality质量 (1-100)80

列出文件

// 列出桶中的文件
const { data, error } = await supabase.storage
.from('avatars')
.list()

console.log(data)
// [
// { name: 'user1.jpg', id: '...', ... },
// { name: 'user2.png', id: '...', ... }
// ]

// 列出特定目录
const { data } = await supabase.storage
.from('avatars')
.list('user123', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})

搜索文件

// 使用搜索模式
const { data } = await supabase.storage
.from('avatars')
.list('', {
search: 'user'
})

移动和复制

移动文件

const { data, error } = await supabase.storage
.from('avatars')
.move('old-path/avatar.jpg', 'new-path/avatar.jpg')

复制文件

const { data, error } = await supabase.storage
.from('avatars')
.copy('source/avatar.jpg', 'destination/avatar.jpg')

删除文件

删除单个文件

const { data, error } = await supabase.storage
.from('avatars')
.remove(['user1.jpg'])

删除多个文件

const { data, error } = await supabase.storage
.from('avatars')
.remove(['user1.jpg', 'user2.png', 'user3.jpg'])

存储策略

与数据库表类似,存储桶也可以使用 RLS 策略控制访问权限。

存储策略表

Supabase 使用以下表管理存储:

  • storage.buckets:存储桶信息
  • storage.objects:文件对象信息

创建存储策略

-- 用户只能访问自己上传的文件
create policy "用户访问自己的文件"
on storage.objects
for all
to authenticated
using (
bucket_id = 'avatars'
and auth.uid()::text = (storage.foldername(name))[1]
);

这个策略假设文件路径格式为 {user_id}/filename

常见存储策略

公开读取,登录上传

-- 公开读取
create policy "公开读取"
on storage.objects
for select
to public
using (bucket_id = 'avatars');

-- 登录用户上传
create policy "登录用户上传"
on storage.objects
for insert
to authenticated
with check (bucket_id = 'avatars');

用户只能管理自己的文件

create policy "用户管理自己的文件"
on storage.objects
for all
to authenticated
using (
bucket_id = 'avatars'
and auth.uid()::text = (storage.foldername(name))[1]
)
with check (
bucket_id = 'avatars'
and auth.uid()::text = (storage.foldername(name))[1]
);

限制文件类型

create policy "只允许图片"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatars'
and (
lower(storage.extension(name)) in ('jpg', 'jpeg', 'png', 'gif', 'webp')
)
);

限制文件大小

create policy "限制文件大小"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'avatars'
and (storage.size(name)) <= 2 * 1024 * 1024 -- 2MB
);

React 组件示例

文件上传组件

import { useState } from 'react'
import { supabase } from '../lib/supabase'

export function FileUpload({ bucket, onUpload }) {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)

const handleUpload = async (event) => {
const file = event.target.files[0]
if (!file) return

setUploading(true)
setProgress(0)

const fileName = `${Date.now()}_${file.name}`

const { data, error } = await supabase.storage
.from(bucket)
.upload(fileName, file, {
onUploadProgress: (progress) => {
const percent = Math.round(
(progress.loaded / progress.total) * 100
)
setProgress(percent)
}
})

setUploading(false)

if (error) {
alert('上传失败: ' + error.message)
} else {
onUpload(data.path)
}
}

return (
<div>
<input
type="file"
onChange={handleUpload}
disabled={uploading}
/>
{uploading && (
<div>
<progress value={progress} max="100" />
<span>{progress}%</span>
</div>
)}
</div>
)
}

图片预览组件

import { supabase } from '../lib/supabase'

export function Avatar({ path, size = 100 }) {
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(path, {
transform: {
width: size,
height: size,
resize: 'cover'
}
})

return (
<img
src={data.publicUrl}
alt="Avatar"
style={{ width: size, height: size, borderRadius: '50%' }}
/>
)
}

图片上传预览

import { useState } from 'react'
import { supabase } from '../lib/supabase'

export function AvatarUpload({ currentAvatar, onUpload }) {
const [uploading, setUploading] = useState(false)
const [preview, setPreview] = useState(null)

const handleFileChange = (event) => {
const file = event.target.files[0]
if (!file) return

// 预览
const reader = new FileReader()
reader.onload = (e) => setPreview(e.target.result)
reader.readAsDataURL(file)

// 上传
uploadAvatar(file)
}

const uploadAvatar = async (file) => {
setUploading(true)

const fileExt = file.name.split('.').pop()
const fileName = `${Date.now()}.${fileExt}`
const filePath = `${supabase.auth.user().id}/${fileName}`

const { error } = await supabase.storage
.from('avatars')
.upload(filePath, file, { upsert: true })

setUploading(false)

if (error) {
alert('上传失败: ' + error.message)
} else {
onUpload(filePath)
}
}

return (
<div>
<img
src={preview || currentAvatar}
alt="Avatar"
style={{ width: 100, height: 100, borderRadius: '50%' }}
/>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
disabled={uploading}
/>
{uploading && <span>上传中...</span>}
</div>
)
}

服务端操作

在服务端可以使用 service_role 密钥管理文件:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
'https://your-project.supabase.co',
process.env.SUPABASE_SERVICE_ROLE_KEY
)

// 列出所有文件
const { data } = await supabase.storage
.from('avatars')
.list('', { limit: 1000 })

// 批量删除
const filesToDelete = data.map(f => f.name)
await supabase.storage
.from('avatars')
.remove(filesToDelete)

最佳实践

文件命名

使用有意义的文件命名策略:

// 使用用户 ID 和时间戳
const fileName = `${userId}/${Date.now()}_${originalName}`

// 使用 UUID
const fileName = `${userId}/${crypto.randomUUID()}.jpg`

// 保留原始扩展名
const ext = file.name.split('.').pop()
const fileName = `${userId}/avatar.${ext}`

错误处理

const { data, error } = await supabase.storage
.from('avatars')
.upload(fileName, file)

if (error) {
if (error.message.includes('already exists')) {
// 文件已存在
} else if (error.message.includes('exceeded')) {
// 超出限制
} else {
// 其他错误
}
}

清理未使用文件

定期清理未关联的文件:

// 获取所有文件
const { data: files } = await supabase.storage
.from('avatars')
.list()

// 获取数据库中引用的文件
const { data: profiles } = await supabase
.from('profiles')
.select('avatar_url')

const usedFiles = new Set(profiles.map(p => p.avatar_url))

// 删除未使用的文件
const unusedFiles = files
.filter(f => !usedFiles.has(f.name))
.map(f => f.name)

if (unusedFiles.length > 0) {
await supabase.storage
.from('avatars')
.remove(unusedFiles)
}

下一步

掌握文件存储后,你可以继续学习: