文件存储
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 中创建
- 进入 Storage 页面
- 点击 "Create a new bucket"
- 填写桶名称
- 选择是否公开访问
- 点击 "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)
}
下一步
掌握文件存储后,你可以继续学习: