- 创建 docs/ 目录存放所有文档 - QUICK_START.md: 快速入门指南 - OPERATIONS_GUIDE.md: 完整运维指南 - VUE_API_INTEGRATION.md: Vue 集成文档 - DEPLOYMENT_INFO.md: 部署配置信息 - versions.md: 版本信息 - 创建 examples/ 目录存放示例代码 - storage_client.py: Python 完整客户端 - storage_client.js: JavaScript 完整客户端 - test_https_storage.py: 功能测试脚本 - 新增 README_STORAGE.md 作为 Storage 使用指南 - 修复签名 URL 生成问题(需要 /storage/v1 前缀) - 测试脚本支持资源已存在的情况 - 所有客户端代码已验证可用 功能特性: ✓ 公网 HTTPS 访问 ✓ 文件上传/下载 ✓ 生成临时下载链接 ✓ 完整的 REST API 客户端 ✓ 支持 Python 和 JavaScript
27 KiB
27 KiB
Supabase API 集成文档 - Vue 前端对接指南
📋 目录
基本信息
🌐 服务地址
Base URL: https://amiap.hzau.edu.cn/supa
内网地址: http://100.64.0.2:18000
Dashboard: http://100.64.0.2:18000 (内网访问)
🔑 认证密钥
// 从 .env 文件获取
const SUPABASE_URL = 'https://amiap.hzau.edu.cn/supa'
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg'
// ⚠️ 仅后端使用!不要暴露给前端
const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NjM4MDI2NjksImV4cCI6MjA3OTE2MjY2OX0.gQWUaTkZ6mjjlv2TED0cODp2meqqWuCGKZR1ptIbovg'
快速开始
1. 安装 Supabase 客户端
npm install @supabase/supabase-js
2. 创建 Supabase 客户端
// src/lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'https://amiap.hzau.edu.cn/supa'
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg'
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
3. 在 Vue 组件中使用
<script setup>
import { supabase } from '@/lib/supabaseClient'
import { ref, onMounted } from 'vue'
const users = ref([])
onMounted(async () => {
const { data, error } = await supabase
.from('users')
.select('*')
if (error) {
console.error('Error:', error)
} else {
users.value = data
}
})
</script>
API 端点
✅ 可用的 API 端点
| 服务 | 端点 | 用途 | 状态 |
|---|---|---|---|
| REST API | https://amiap.hzau.edu.cn/supa/rest/v1/ |
数据库操作 | ✅ 可用 |
| Auth API | https://amiap.hzau.edu.cn/supa/auth/v1/ |
用户认证 | ✅ 可用 |
| Storage API | https://amiap.hzau.edu.cn/supa/storage/v1/ |
文件存储 | ✅ 可用 |
| Realtime | wss://amiap.hzau.edu.cn/supa/realtime/v1/ |
实时订阅 | ✅ 可用 |
| Edge Functions | https://amiap.hzau.edu.cn/supa/functions/v1/ |
云函数 | ⚠️ 需配置 |
认证配置
环境变量配置
创建 .env 文件:
VITE_SUPABASE_URL=https://amiap.hzau.edu.cn/supa
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg
使用环境变量
// src/lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
REST API 使用
查询数据
// 查询所有记录
const { data, error } = await supabase
.from('users')
.select('*')
// 条件查询
const { data, error } = await supabase
.from('users')
.select('id, name, email')
.eq('status', 'active')
.order('created_at', { ascending: false })
.limit(10)
// 关联查询
const { data, error } = await supabase
.from('posts')
.select(`
*,
author:users(name, email),
comments(*)
`)
插入数据
const { data, error } = await supabase
.from('users')
.insert([
{ name: 'John Doe', email: 'john@example.com' }
])
.select()
更新数据
const { data, error } = await supabase
.from('users')
.update({ status: 'inactive' })
.eq('id', userId)
.select()
删除数据
const { data, error } = await supabase
.from('users')
.delete()
.eq('id', userId)
Auth API 使用
用户注册
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
data: {
first_name: 'John',
last_name: 'Doe'
}
}
})
用户登录
// 邮箱密码登录
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123'
})
// OAuth 登录(需要配置)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google'
})
获取当前用户
const { data: { user } } = await supabase.auth.getUser()
退出登录
const { error } = await supabase.auth.signOut()
监听认证状态
supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session)
if (event === 'SIGNED_IN') {
console.log('用户已登录:', session.user)
}
if (event === 'SIGNED_OUT') {
console.log('用户已退出')
}
})
Vue 3 Composition API 示例
<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from '@/lib/supabaseClient'
import { useRouter } from 'vue-router'
const router = useRouter()
const email = ref('')
const password = ref('')
const user = ref(null)
// 登录
const signIn = async () => {
const { data, error } = await supabase.auth.signInWithPassword({
email: email.value,
password: password.value
})
if (error) {
alert('登录失败: ' + error.message)
} else {
user.value = data.user
router.push('/dashboard')
}
}
// 退出
const signOut = async () => {
await supabase.auth.signOut()
user.value = null
router.push('/login')
}
// 检查当前用户
onMounted(async () => {
const { data: { user: currentUser } } = await supabase.auth.getUser()
user.value = currentUser
// 监听认证状态变化
supabase.auth.onAuthStateChange((event, session) => {
user.value = session?.user || null
})
})
</script>
<template>
<div v-if="!user">
<input v-model="email" type="email" placeholder="邮箱" />
<input v-model="password" type="password" placeholder="密码" />
<button @click="signIn">登录</button>
</div>
<div v-else>
<p>欢迎, {{ user.email }}</p>
<button @click="signOut">退出</button>
</div>
</template>
Storage API 使用
上传文件
const file = event.target.files[0]
const fileName = `${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('avatars')
.upload(fileName, file)
if (error) {
console.error('上传失败:', error)
} else {
console.log('上传成功:', data)
}
下载文件
const { data, error } = await supabase.storage
.from('avatars')
.download('path/to/file.jpg')
获取公开 URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('path/to/file.jpg')
console.log('公开URL:', data.publicUrl)
删除文件
const { data, error } = await supabase.storage
.from('avatars')
.remove(['path/to/file.jpg'])
Vue 文件上传组件示例
<script setup>
import { ref } from 'vue'
import { supabase } from '@/lib/supabaseClient'
const uploading = ref(false)
const fileUrl = ref('')
const uploadFile = async (event) => {
try {
uploading.value = true
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `${fileName}`
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file)
if (uploadError) throw uploadError
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(filePath)
fileUrl.value = data.publicUrl
} catch (error) {
alert('上传失败: ' + error.message)
} finally {
uploading.value = false
}
}
</script>
<template>
<div>
<input type="file" @change="uploadFile" :disabled="uploading" />
<div v-if="uploading">上传中...</div>
<img v-if="fileUrl" :src="fileUrl" alt="上传的图片" />
</div>
</template>
Realtime 使用
订阅数据变化
// 订阅表的所有变化
const channel = supabase
.channel('public:users')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'users' },
(payload) => {
console.log('数据变化:', payload)
}
)
.subscribe()
// 订阅特定操作
const channel = supabase
.channel('public:posts')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
console.log('新增记录:', payload.new)
}
)
.on('postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'posts' },
(payload) => {
console.log('更新记录:', payload.new)
}
)
.on('postgres_changes',
{ event: 'DELETE', schema: 'public', table: 'posts' },
(payload) => {
console.log('删除记录:', payload.old)
}
)
.subscribe()
取消订阅
const removeChannel = async () => {
await supabase.removeChannel(channel)
}
Vue 实时数据组件示例
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { supabase } from '@/lib/supabaseClient'
const messages = ref([])
let channel = null
onMounted(async () => {
// 加载初始数据
const { data } = await supabase
.from('messages')
.select('*')
.order('created_at', { ascending: false })
messages.value = data || []
// 订阅实时更新
channel = supabase
.channel('public:messages')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => {
messages.value = [payload.new, ...messages.value]
}
)
.on('postgres_changes',
{ event: 'DELETE', schema: 'public', table: 'messages' },
(payload) => {
messages.value = messages.value.filter(m => m.id !== payload.old.id)
}
)
.subscribe()
})
onUnmounted(() => {
if (channel) {
supabase.removeChannel(channel)
}
})
</script>
<template>
<div>
<h2>实时消息</h2>
<ul>
<li v-for="message in messages" :key="message.id">
{{ message.content }}
</li>
</ul>
</div>
</template>
完整示例
完整的 CRUD 示例
<script setup>
import { ref, onMounted } from 'vue'
import { supabase } from '@/lib/supabaseClient'
// 状态
const items = ref([])
const loading = ref(false)
const newItem = ref({ name: '', description: '' })
// 读取数据
const fetchItems = async () => {
loading.value = true
const { data, error } = await supabase
.from('items')
.select('*')
.order('created_at', { ascending: false })
if (error) {
console.error('获取数据失败:', error)
} else {
items.value = data
}
loading.value = false
}
// 创建数据
const createItem = async () => {
const { data, error } = await supabase
.from('items')
.insert([newItem.value])
.select()
if (error) {
alert('创建失败: ' + error.message)
} else {
items.value = [data[0], ...items.value]
newItem.value = { name: '', description: '' }
}
}
// 更新数据
const updateItem = async (id, updates) => {
const { data, error } = await supabase
.from('items')
.update(updates)
.eq('id', id)
.select()
if (error) {
alert('更新失败: ' + error.message)
} else {
const index = items.value.findIndex(item => item.id === id)
if (index !== -1) {
items.value[index] = data[0]
}
}
}
// 删除数据
const deleteItem = async (id) => {
if (!confirm('确认删除?')) return
const { error } = await supabase
.from('items')
.delete()
.eq('id', id)
if (error) {
alert('删除失败: ' + error.message)
} else {
items.value = items.value.filter(item => item.id !== id)
}
}
onMounted(() => {
fetchItems()
})
</script>
<template>
<div>
<h1>数据管理</h1>
<!-- 创建表单 -->
<div class="form">
<input v-model="newItem.name" placeholder="名称" />
<input v-model="newItem.description" placeholder="描述" />
<button @click="createItem">创建</button>
</div>
<!-- 列表 -->
<div v-if="loading">加载中...</div>
<ul v-else>
<li v-for="item in items" :key="item.id">
<span>{{ item.name }} - {{ item.description }}</span>
<button @click="updateItem(item.id, { name: '已更新' })">更新</button>
<button @click="deleteItem(item.id)">删除</button>
</li>
</ul>
</div>
</template>
错误处理
统一错误处理
// src/lib/errorHandler.js
export const handleSupabaseError = (error) => {
if (!error) return null
// 认证错误
if (error.status === 401) {
console.error('认证失败,请重新登录')
// 跳转到登录页
router.push('/login')
}
// 权限错误
if (error.status === 403) {
console.error('权限不足')
}
// 网络错误
if (error.message && error.message.includes('fetch')) {
console.error('网络连接失败')
}
return error.message || '操作失败'
}
使用错误处理
import { handleSupabaseError } from '@/lib/errorHandler'
const fetchData = async () => {
const { data, error } = await supabase
.from('users')
.select('*')
if (error) {
const errorMsg = handleSupabaseError(error)
alert(errorMsg)
return
}
// 处理成功的数据
console.log(data)
}
🔧 数据库 Schema 示例
创建表(在 Dashboard 中执行)
-- 创建用户表
CREATE TABLE users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 创建帖子表
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 启用 RLS (Row Level Security)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 创建策略
CREATE POLICY "用户可以查看所有用户" ON users
FOR SELECT USING (true);
CREATE POLICY "用户只能更新自己的数据" ON users
FOR UPDATE USING (auth.uid() = id);
CREATE POLICY "所有人可以查看已发布的帖子" ON posts
FOR SELECT USING (published = true);
CREATE POLICY "用户可以创建自己的帖子" ON posts
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "用户可以更新自己的帖子" ON posts
FOR UPDATE USING (auth.uid() = user_id);
📚 更多资源
⚠️ 注意事项
- ANON_KEY 是公开的:可以在前端代码中使用
- SERVICE_ROLE_KEY 必须保密:只能在后端使用
- 使用 RLS (Row Level Security):保护数据安全
- Dashboard 访问:只能通过内网
http://100.64.0.2:18000访问 - API 端点:通过
https://amiap.hzau.edu.cn/supa访问所有服务
🎯 下一步
- 在 Dashboard 中创建数据库表
- 配置 RLS 策略
- 在 Vue 应用中集成 Supabase
- 开发认证、CRUD、文件上传等功能
- 测试实时订阅功能
📦 S3 对象存储配置与使用
概述
Supabase Storage 已配置使用 MinIO 作为 S3 兼容的对象存储后端。所有文件都会保存在宿主机的 /vol1/1000/s3 目录。
🔧 启动 S3 存储服务
cd /vol1/1000/docker_server/traefik/supabase-stack
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
说明:
docker-compose.yml: 主配置文件(包含所有 Supabase 服务)docker-compose.s3.yml: MinIO 配置文件(在/vol1/1000/docker_server/traefik/supabase-stack/目录)- 所有对象文件实际存储在:
/vol1/1000/s3/stub/<bucket_name>/...
🌐 S3 访问端点
统一 S3 端点(推荐)
https://amiap.hzau.edu.cn/supa/storage/v1/s3
这是标准的 Supabase Storage S3 协议端点,完全兼容 AWS S3 API。
MinIO 管理控制台(可选)
http://100.64.0.2:9001
用户名: supa-storage
密码: secret519521
🔑 S3 认证密钥
1. S3 协议访问密钥(推荐用于应用)
用于通过 Supabase Storage 的 S3 端点访问:
# 从 .env 文件
S3_PROTOCOL_ACCESS_KEY_ID=supa-protocol-key
S3_PROTOCOL_ACCESS_KEY_SECRET=supa-protocol-secret
S3_PROTOCOL_REGION=stub
2. MinIO 根密钥(内部使用)
用于 storage 服务连接 MinIO,一般不直接使用:
MINIO_ROOT_USER=supa-storage
MINIO_ROOT_PASSWORD=secret1234
💻 客户端配置示例
Python (boto3)
import boto3
from botocore.client import Config
# 配置 S3 客户端连接到 Supabase Storage
s3_client = boto3.client(
's3',
endpoint_url='https://amiap.hzau.edu.cn/supa/storage/v1/s3',
aws_access_key_id='supa-protocol-key',
aws_secret_access_key='supa-protocol-secret',
region_name='stub',
config=Config(
signature_version='s3v4',
s3={'addressing_style': 'path'}
),
verify=True # HTTPS 验证
)
# 列出所有 buckets
response = s3_client.list_buckets()
for bucket in response['Buckets']:
print(f"Bucket: {bucket['Name']}")
# 上传文件
with open('example.jpg', 'rb') as file:
s3_client.upload_fileobj(
file,
'my-bucket', # bucket 名称
'uploads/example.jpg' # 对象路径
)
# 下载文件
s3_client.download_file(
'my-bucket',
'uploads/example.jpg',
'downloaded.jpg'
)
# 生成预签名 URL(临时访问链接)
url = s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': 'my-bucket', 'Key': 'uploads/example.jpg'},
ExpiresIn=3600 # 1小时过期
)
print(f"Presigned URL: {url}")
JavaScript/Node.js (AWS SDK v3)
import { S3Client, ListBucketsCommand, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import fs from 'fs'
// 配置 S3 客户端
const s3Client = new S3Client({
endpoint: 'https://amiap.hzau.edu.cn/supa/storage/v1/s3',
region: 'stub',
credentials: {
accessKeyId: 'supa-protocol-key',
secretAccessKey: 'supa-protocol-secret',
},
forcePathStyle: true, // 使用路径风格访问
})
// 列出 buckets
const listBuckets = async () => {
const command = new ListBucketsCommand({})
const response = await s3Client.send(command)
console.log('Buckets:', response.Buckets)
}
// 上传文件
const uploadFile = async () => {
const fileContent = fs.readFileSync('example.jpg')
const command = new PutObjectCommand({
Bucket: 'my-bucket',
Key: 'uploads/example.jpg',
Body: fileContent,
ContentType: 'image/jpeg',
})
const response = await s3Client.send(command)
console.log('Upload success:', response)
}
// 生成预签名 URL
const getPresignedUrl = async () => {
const command = new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'uploads/example.jpg',
})
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 })
console.log('Presigned URL:', url)
return url
}
// 执行
await listBuckets()
await uploadFile()
await getPresignedUrl()
PostgreSQL Foreign Data Wrapper (FDW)
在 PostgreSQL 中直接查询 S3 对象:
-- 1. 启用扩展
CREATE EXTENSION IF NOT EXISTS wrappers CASCADE;
-- 2. 创建 S3 服务器连接
CREATE SERVER s3_server
FOREIGN DATA WRAPPER s3_wrapper
OPTIONS (
aws_access_key_id 'supa-protocol-key',
aws_secret_access_key 'supa-protocol-secret',
aws_region 'stub',
endpoint_url 'https://amiap.hzau.edu.cn/supa/storage/v1/s3',
path_style_url 'true'
);
-- 3. 创建外部表映射到 S3 对象(例如 Parquet 文件)
CREATE FOREIGN TABLE my_s3_table (
id bigint,
name text,
created_at timestamp
)
SERVER s3_server
OPTIONS (
uri 's3://my-bucket/data/file.parquet',
format 'parquet'
);
-- 4. 查询 S3 数据
SELECT * FROM my_s3_table WHERE created_at > NOW() - INTERVAL '7 days';
AWS CLI
# 配置 AWS CLI
export AWS_ACCESS_KEY_ID=supa-protocol-key
export AWS_SECRET_ACCESS_KEY=supa-protocol-secret
export AWS_DEFAULT_REGION=stub
# 或创建 AWS 配置文件 ~/.aws/config
# [profile supabase]
# region = stub
# output = json
# ~/.aws/credentials
# [supabase]
# aws_access_key_id = supa-protocol-key
# aws_secret_access_key = supa-protocol-secret
# 列出 buckets
aws s3 ls --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
# 列出 bucket 中的对象
aws s3 ls s3://my-bucket/ --endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
# 上传文件
aws s3 cp example.jpg s3://my-bucket/uploads/ \
--endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
# 下载文件
aws s3 cp s3://my-bucket/uploads/example.jpg downloaded.jpg \
--endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
# 同步目录
aws s3 sync ./local-dir s3://my-bucket/backup/ \
--endpoint-url=https://amiap.hzau.edu.cn/supa/storage/v1/s3
Vue 3 + Supabase Client(推荐)
<script setup>
import { ref } from 'vue'
import { supabase } from '@/lib/supabaseClient'
const uploading = ref(false)
const uploadedUrl = ref('')
// 上传文件到 S3
const uploadToS3 = async (event) => {
try {
uploading.value = true
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random()}.${fileExt}`
const filePath = `uploads/${fileName}`
// Supabase Storage 会自动使用配置的 S3 后端(MinIO)
const { data, error } = await supabase.storage
.from('my-bucket') // bucket 名称
.upload(filePath, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
// 获取公开 URL
const { data: urlData } = supabase.storage
.from('my-bucket')
.getPublicUrl(filePath)
uploadedUrl.value = urlData.publicUrl
console.log('文件已上传到 S3:', uploadedUrl.value)
} catch (error) {
console.error('上传失败:', error.message)
} finally {
uploading.value = false
}
}
// 从 S3 下载文件
const downloadFromS3 = async (filePath) => {
const { data, error } = await supabase.storage
.from('my-bucket')
.download(filePath)
if (error) {
console.error('下载失败:', error)
return
}
// 创建下载链接
const url = URL.createObjectURL(data)
const a = document.createElement('a')
a.href = url
a.download = filePath.split('/').pop()
a.click()
}
// 列出 bucket 中的文件
const listFiles = async () => {
const { data, error } = await supabase.storage
.from('my-bucket')
.list('uploads/', {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
if (error) {
console.error('列表失败:', error)
return
}
console.log('S3 文件列表:', data)
return data
}
</script>
<template>
<div>
<h2>S3 文件上传</h2>
<input type="file" @change="uploadToS3" :disabled="uploading" />
<div v-if="uploading">上传中...</div>
<div v-if="uploadedUrl">
<p>上传成功!</p>
<a :href="uploadedUrl" target="_blank">查看文件</a>
</div>
</div>
</template>
🗂️ 数据存储位置
所有上传的文件实际保存在:
/vol1/1000/s3/stub/<bucket_name>/<object_path>
示例:
/vol1/1000/s3/
└── stub/
├── my-bucket/
│ ├── uploads/
│ │ ├── image1.jpg
│ │ └── image2.png
│ └── documents/
│ └── report.pdf
└── avatars/
└── user123.jpg
🔒 安全建议
- 生产环境密钥:修改
.env中的默认密钥 - HTTPS 访问:外部访问使用 HTTPS 端点
- Bucket 权限:合理配置 RLS 策略
- MinIO 控制台:仅内网访问(端口 9001)
❓ 常见问题
Q: 文件存在哪里?
A: 所有文件物理存储在 /vol1/1000/s3/stub/<bucket_name>/
Q: 如何备份数据?
A: 直接备份 /vol1/1000/s3 目录即可
Q: 可以直接访问 MinIO 吗? A: 可以,但推荐使用 Supabase Storage 的 S3 端点(更安全)
Q: 如何迁移现有文件到 S3? A: 使用 AWS CLI 或 Python 脚本批量上传到对应 bucket
📝 在 Dashboard 中创建 Bucket
- 访问 Dashboard:
http://100.64.0.2:18000 - 登录后进入 Storage 页面
- 点击 New Bucket
- 输入 bucket 名称(例如:
my-bucket) - 配置访问权限(Public/Private)
- 创建完成后,文件会保存在
/vol1/1000/s3/stub/my-bucket/
⚙️ 配置文件说明
.env 配置
# MinIO 内部认证
MINIO_ROOT_USER=supa-storage
MINIO_ROOT_PASSWORD=secret1234
# S3 协议访问密钥(用于外部客户端)
S3_PROTOCOL_ACCESS_KEY_ID=supa-protocol-key
S3_PROTOCOL_ACCESS_KEY_SECRET=supa-protocol-secret
S3_PROTOCOL_REGION=stub
# S3 配置
GLOBAL_S3_BUCKET=stub
GLOBAL_S3_ENDPOINT=http://minio:9000
GLOBAL_S3_PROTOCOL=http
GLOBAL_S3_FORCE_PATH_STYLE=true
AWS_DEFAULT_REGION=stub
docker-compose.s3.yml
services:
minio:
image: minio/minio
ports:
- "9000:9000" # S3 API
- "9001:9001" # Web 控制台
environment:
MINIO_ROOT_USER: supa-storage
MINIO_ROOT_PASSWORD: secret1234
command: server --console-address ":9001" /data
volumes:
- /vol1/1000/s3:/data:z # 数据存储位置
minio-createbucket:
image: minio/mc
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set supa-minio http://minio:9000 supa-storage secret1234;
/usr/bin/mc mb supa-minio/stub;
exit 0;
"
🔧 验证 S3 配置
1. 检查 MinIO 服务状态
docker compose ps minio
2. 使用 MinIO Client 验证
# 进入 MinIO 容器
docker exec -it $(docker ps -qf "name=minio") sh
# 配置 mc 别名
mc alias set local http://localhost:9000 supa-storage secret1234
# 列出 buckets
mc ls local/
# 查看 stub bucket
mc ls local/stub/
# 退出
exit
3. 测试 S3 API 端点
# 测试 endpoint 是否可访问
curl -I https://amiap.hzau.edu.cn/supa/storage/v1/s3