Supabase文件上传下载 #
一、文件上传 #
1.1 基础上传 #
typescript
// 上传文件
const { data, error } = await supabase.storage
.from('avatars')
.upload('user-123/avatar.jpg', file)
// data: { path: 'user-123/avatar.jpg', ... }
1.2 带配置上传 #
typescript
const { data, error } = await supabase.storage
.from('avatars')
.upload('user-123/avatar.jpg', file, {
cacheControl: '3600', // 缓存时间
contentType: 'image/jpeg', // 内容类型
upsert: true, // 覆盖已存在文件
})
1.3 上传表单示例 #
tsx
import { useState } from 'react'
import { supabase } from '../lib/supabase'
export function FileUpload() {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
setProgress(0)
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const fileExt = file.name.split('.').pop()
const fileName = `${user.id}/${Date.now()}.${fileExt}`
const { data, error } = await supabase.storage
.from('avatars')
.upload(fileName, file, {
upsert: true,
})
setUploading(false)
if (error) {
console.error('Upload error:', error)
} else {
console.log('Uploaded:', data.path)
}
}
return (
<div>
<input
type="file"
accept="image/*"
onChange={handleUpload}
disabled={uploading}
/>
{uploading && <div>Uploading... {progress}%</div>}
</div>
)
}
1.4 拖拽上传 #
tsx
import { useState, useCallback } from 'react'
import { supabase } from '../lib/supabase'
export function DropZone() {
const [dragging, setDragging] = useState(false)
const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault()
setDragging(false)
const files = Array.from(e.dataTransfer.files)
for (const file of files) {
await uploadFile(file)
}
}, [])
async function uploadFile(file: File) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const path = `${user.id}/${Date.now()}-${file.name}`
const { error } = await supabase.storage
.from('uploads')
.upload(path, file)
if (error) {
console.error('Upload error:', error)
}
}
return (
<div
onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
style={{
border: dragging ? '2px dashed blue' : '2px dashed gray',
padding: '40px',
textAlign: 'center',
}}
>
Drop files here
</div>
)
}
二、文件下载 #
2.1 获取公开URL #
typescript
// 公开桶直接获取URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user-123/avatar.jpg')
console.log(data.publicUrl)
// https://xxx.supabase.co/storage/v1/object/public/avatars/user-123/avatar.jpg
2.2 获取签名URL #
typescript
// 私有桶需要签名URL
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl('user-123/report.pdf', 3600) // 1小时有效
console.log(data.signedUrl)
2.3 下载文件 #
typescript
// 下载文件为Blob
const { data, error } = await supabase.storage
.from('documents')
.download('user-123/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)
}
2.4 下载组件示例 #
tsx
import { supabase } from '../lib/supabase'
export function DownloadButton({ path, filename }: { path: string; filename: string }) {
const handleDownload = async () => {
const { data, error } = await supabase.storage
.from('documents')
.download(path)
if (data) {
const url = URL.createObjectURL(data)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
}
return (
<button onClick={handleDownload}>
Download
</button>
)
}
三、文件列表 #
3.1 列出文件 #
typescript
// 列出文件夹中的文件
const { data, error } = await supabase.storage
.from('avatars')
.list('user-123', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' },
})
data?.forEach(file => {
console.log(file.name)
console.log(file.metadata?.size)
console.log(file.metadata?.mimetype)
})
3.2 递归列出所有文件 #
typescript
async function listAllFiles(bucket: string, folder: string = ''): Promise<string[]> {
const files: string[] = []
const { data } = await supabase.storage
.from(bucket)
.list(folder)
for (const item of data || []) {
const path = folder ? `${folder}/${item.name}` : item.name
if (item.metadata) {
// 是文件
files.push(path)
} else {
// 是文件夹,递归
const subFiles = await listAllFiles(bucket, path)
files.push(...subFiles)
}
}
return files
}
3.3 文件列表组件 #
tsx
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
export function FileList({ bucket, folder }: { bucket: string; folder: string }) {
const [files, setFiles] = useState<any[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadFiles()
}, [bucket, folder])
async function loadFiles() {
setLoading(true)
const { data } = await supabase.storage
.from(bucket)
.list(folder)
setFiles(data || [])
setLoading(false)
}
if (loading) return <div>Loading...</div>
return (
<ul>
{files.map(file => (
<li key={file.name}>
{file.name} ({formatBytes(file.metadata?.size)})
</li>
))}
</ul>
)
}
function formatBytes(bytes: number): string {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
四、文件删除 #
4.1 删除单个文件 #
typescript
const { data, error } = await supabase.storage
.from('avatars')
.remove(['user-123/avatar.jpg'])
4.2 批量删除 #
typescript
const { data, error } = await supabase.storage
.from('avatars')
.remove([
'user-123/avatar1.jpg',
'user-123/avatar2.jpg',
'user-123/avatar3.jpg',
])
4.3 删除文件夹 #
typescript
async function deleteFolder(bucket: string, folder: string) {
// 1. 列出所有文件
const files = await listAllFiles(bucket, folder)
// 2. 批量删除
if (files.length > 0) {
await supabase.storage
.from(bucket)
.remove(files)
}
}
五、文件移动和复制 #
5.1 移动文件 #
typescript
const { data, error } = await supabase.storage
.from('avatars')
.move('user-123/old.jpg', 'user-123/new.jpg')
5.2 复制文件 #
typescript
const { data, error } = await supabase.storage
.from('documents')
.copy('user-123/original.pdf', 'user-123/copy.pdf')
六、大文件上传 #
6.1 分片上传 #
typescript
async function uploadLargeFile(
bucket: string,
path: string,
file: File,
chunkSize: number = 5 * 1024 * 1024 // 5MB
) {
const totalChunks = Math.ceil(file.size / chunkSize)
const uploadId = crypto.randomUUID()
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
const chunkPath = `${path}.part.${i}`
await supabase.storage
.from(bucket)
.upload(chunkPath, chunk)
console.log(`Uploaded chunk ${i + 1}/${totalChunks}`)
}
// 合并分片(需要服务端处理)
// ...
}
6.2 上传进度 #
typescript
// 使用XMLHttpRequest获取进度
function uploadWithProgress(
bucket: string,
path: string,
file: File,
onProgress: (percent: number) => void
): Promise<any> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100)
onProgress(percent)
}
})
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(xhr.statusText))
}
})
xhr.addEventListener('error', () => reject(new Error('Upload failed')))
xhr.open('POST', `${supabaseUrl}/storage/v1/object/${bucket}/${path}`)
xhr.setRequestHeader('Authorization', `Bearer ${supabaseKey}`)
xhr.setRequestHeader('Content-Type', file.type)
xhr.send(file)
})
}
七、错误处理 #
7.1 常见错误 #
typescript
const errorMessages: Record<string, string> = {
'Bucket not found': '存储桶不存在',
'File not found': '文件不存在',
'File already exists': '文件已存在',
'storage quota exceeded': '存储配额已满',
'File size exceeded': '文件大小超限',
'Invalid mime type': '文件类型不允许',
'Unauthorized': '未授权访问',
}
function handleStorageError(error: any): string {
return errorMessages[error.message] || error.message
}
7.2 重试机制 #
typescript
async function uploadWithRetry(
bucket: string,
path: string,
file: File,
maxRetries: number = 3
) {
let lastError: any
for (let i = 0; i < maxRetries; i++) {
try {
const result = await supabase.storage
.from(bucket)
.upload(path, file)
if (!result.error) {
return result
}
lastError = result.error
} catch (err) {
lastError = err
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
throw lastError
}
八、最佳实践 #
8.1 文件命名 #
typescript
function generateUniquePath(userId: string, filename: string): string {
const ext = filename.split('.').pop()
const timestamp = Date.now()
const random = Math.random().toString(36).substring(7)
return `${userId}/${timestamp}-${random}.${ext}`
}
8.2 文件验证 #
typescript
function validateFile(
file: File,
options: {
maxSize?: number
allowedTypes?: string[]
}
): { valid: boolean; error?: string } {
if (options.maxSize && file.size > options.maxSize) {
return { valid: false, error: 'File too large' }
}
if (options.allowedTypes) {
const allowed = options.allowedTypes.some(type => {
if (type.endsWith('/*')) {
return file.type.startsWith(type.slice(0, -1))
}
return file.type === type
})
if (!allowed) {
return { valid: false, error: 'File type not allowed' }
}
}
return { valid: true }
}
九、总结 #
文件操作要点:
| 操作 | 方法 |
|---|---|
| 上传 | upload(path, file) |
| 下载 | download(path) |
| 列表 | list(folder) |
| 删除 | remove([paths]) |
| 移动 | move(from, to) |
| 复制 | copy(from, to) |
| 公开URL | getPublicUrl(path) |
| 签名URL | createSignedUrl(path, expiresIn) |
下一步,让我们学习图片处理!
最后更新:2026-03-28