Files
labweb/supabase-stack/docs/VUE_API_INTEGRATION.md
zly 9fa602f21b feat(supabase): 整理 Storage 文档和示例代码
- 创建 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
2025-11-22 21:03:00 +08:00

1239 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Supabase API 集成文档 - Vue 前端对接指南
## 📋 目录
- [基本信息](#基本信息)
- [快速开始](#快速开始)
- [API 端点](#api-端点)
- [认证配置](#认证配置)
- [REST API 使用](#rest-api-使用)
- [Auth API 使用](#auth-api-使用)
- [Storage API 使用](#storage-api-使用)
- [Realtime 使用](#realtime-使用)
- [完整示例](#完整示例)
- [错误处理](#错误处理)
---
## 基本信息
### 🌐 服务地址
```
Base URL: https://amiap.hzau.edu.cn/supa
内网地址: http://100.64.0.2:18000
Dashboard: http://100.64.0.2:18000 (内网访问)
```
### 🔑 认证密钥
```javascript
// 从 .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 客户端
```bash
npm install @supabase/supabase-js
```
### 2. 创建 Supabase 客户端
```javascript
// 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 组件中使用
```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` 文件:
```env
VITE_SUPABASE_URL=https://amiap.hzau.edu.cn/supa
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg
```
### 使用环境变量
```javascript
// 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 使用
### 查询数据
```javascript
// 查询所有记录
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(*)
`)
```
### 插入数据
```javascript
const { data, error } = await supabase
.from('users')
.insert([
{ name: 'John Doe', email: 'john@example.com' }
])
.select()
```
### 更新数据
```javascript
const { data, error } = await supabase
.from('users')
.update({ status: 'inactive' })
.eq('id', userId)
.select()
```
### 删除数据
```javascript
const { data, error } = await supabase
.from('users')
.delete()
.eq('id', userId)
```
---
## Auth API 使用
### 用户注册
```javascript
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
data: {
first_name: 'John',
last_name: 'Doe'
}
}
})
```
### 用户登录
```javascript
// 邮箱密码登录
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123'
})
// OAuth 登录(需要配置)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google'
})
```
### 获取当前用户
```javascript
const { data: { user } } = await supabase.auth.getUser()
```
### 退出登录
```javascript
const { error } = await supabase.auth.signOut()
```
### 监听认证状态
```javascript
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 示例
```vue
<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 使用
### 上传文件
```javascript
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)
}
```
### 下载文件
```javascript
const { data, error } = await supabase.storage
.from('avatars')
.download('path/to/file.jpg')
```
### 获取公开 URL
```javascript
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('path/to/file.jpg')
console.log('公开URL:', data.publicUrl)
```
### 删除文件
```javascript
const { data, error } = await supabase.storage
.from('avatars')
.remove(['path/to/file.jpg'])
```
### Vue 文件上传组件示例
```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 使用
### 订阅数据变化
```javascript
// 订阅表的所有变化
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()
```
### 取消订阅
```javascript
const removeChannel = async () => {
await supabase.removeChannel(channel)
}
```
### Vue 实时数据组件示例
```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 示例
```vue
<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>
```
---
## 错误处理
### 统一错误处理
```javascript
// 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 || '操作失败'
}
```
### 使用错误处理
```javascript
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 中执行)
```sql
-- 创建用户表
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);
```
---
## 📚 更多资源
- [Supabase 官方文档](https://supabase.com/docs)
- [Supabase JS 客户端文档](https://supabase.com/docs/reference/javascript)
- [Vue 3 官方文档](https://vuejs.org/)
---
## ⚠️ 注意事项
1. **ANON_KEY 是公开的**:可以在前端代码中使用
2. **SERVICE_ROLE_KEY 必须保密**:只能在后端使用
3. **使用 RLS (Row Level Security)**:保护数据安全
4. **Dashboard 访问**:只能通过内网 `http://100.64.0.2:18000` 访问
5. **API 端点**:通过 `https://amiap.hzau.edu.cn/supa` 访问所有服务
---
## 🎯 下一步
1. 在 Dashboard 中创建数据库表
2. 配置 RLS 策略
3. 在 Vue 应用中集成 Supabase
4. 开发认证、CRUD、文件上传等功能
5. 测试实时订阅功能
---
## 📦 S3 对象存储配置与使用
### 概述
Supabase Storage 已配置使用 **MinIO** 作为 S3 兼容的对象存储后端。所有文件都会保存在宿主机的 `/vol1/1000/s3` 目录。
### 🔧 启动 S3 存储服务
```bash
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 端点访问:
```bash
# 从 .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一般不直接使用
```bash
MINIO_ROOT_USER=supa-storage
MINIO_ROOT_PASSWORD=secret1234
```
---
### 💻 客户端配置示例
#### Python (boto3)
```python
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)
```javascript
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 对象:
```sql
-- 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
```bash
# 配置 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推荐
```vue
<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
```
---
### 🔒 安全建议
1. **生产环境密钥**:修改 `.env` 中的默认密钥
2. **HTTPS 访问**:外部访问使用 HTTPS 端点
3. **Bucket 权限**:合理配置 RLS 策略
4. **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
1. 访问 Dashboard: `http://100.64.0.2:18000`
2. 登录后进入 **Storage** 页面
3. 点击 **New Bucket**
4. 输入 bucket 名称(例如:`my-bucket`
5. 配置访问权限Public/Private
6. 创建完成后,文件会保存在 `/vol1/1000/s3/stub/my-bucket/`
---
### ⚙️ 配置文件说明
#### .env 配置
```bash
# 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
```yaml
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 服务状态
```bash
docker compose ps minio
```
#### 2. 使用 MinIO Client 验证
```bash
# 进入 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 端点
```bash
# 测试 endpoint 是否可访问
curl -I https://amiap.hzau.edu.cn/supa/storage/v1/s3
```
---
### 📚 相关资源
- [Supabase Storage 文档](https://supabase.com/docs/guides/storage)
- [MinIO 文档](https://min.io/docs/)
- [AWS S3 API 文档](https://docs.aws.amazon.com/s3/)
- [boto3 文档](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)