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
This commit is contained in:
237
supabase-stack/README_STORAGE.md
Normal file
237
supabase-stack/README_STORAGE.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# Supabase Storage 对象存储使用指南
|
||||||
|
|
||||||
|
完整的 Supabase 对象存储解决方案,支持公网 HTTPS 访问。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 文档导航
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
- **[快速入门](docs/QUICK_START.md)** - 5 分钟快速上手指南
|
||||||
|
- **[运维指南](docs/OPERATIONS_GUIDE.md)** - 完整的系统架构和运维文档
|
||||||
|
|
||||||
|
### 集成文档
|
||||||
|
- **[Vue API 集成](docs/VUE_API_INTEGRATION.md)** - Vue 应用集成指南
|
||||||
|
- **[部署信息](docs/DEPLOYMENT_INFO.md)** - 部署配置详情
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 客户端代码
|
||||||
|
|
||||||
|
所有示例代码位于 `examples/` 目录:
|
||||||
|
|
||||||
|
### Python 客户端
|
||||||
|
```bash
|
||||||
|
# 查看完整客户端代码
|
||||||
|
cat examples/storage_client.py
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
python3 << 'EOF'
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'examples')
|
||||||
|
from storage_client import SupabaseStorageClient
|
||||||
|
|
||||||
|
client = SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
client.upload_file('bucket', 'photo.jpg', 'uploads/photo.jpg')
|
||||||
|
|
||||||
|
# 生成临时下载链接
|
||||||
|
url = client.create_signed_url('bucket', 'uploads/photo.jpg', 3600)
|
||||||
|
print(f'临时下载链接: {url}')
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript 客户端
|
||||||
|
```bash
|
||||||
|
# 查看完整客户端代码
|
||||||
|
cat examples/storage_client.js
|
||||||
|
|
||||||
|
# 在网页中使用
|
||||||
|
<script src="examples/storage_client.js"></script>
|
||||||
|
<script>
|
||||||
|
const client = new SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
await client.uploadFile('bucket', fileObject, 'uploads/photo.jpg');
|
||||||
|
|
||||||
|
// 生成临时下载链接
|
||||||
|
const url = await client.createSignedUrl('bucket', 'uploads/photo.jpg', 3600);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试
|
||||||
|
|
||||||
|
运行完整的功能测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试所有 Storage API 功能
|
||||||
|
python3 examples/test_https_storage.py
|
||||||
|
```
|
||||||
|
|
||||||
|
测试包含:
|
||||||
|
- ✓ 列出 Buckets
|
||||||
|
- ✓ 创建 Bucket
|
||||||
|
- ✓ 上传/更新文件
|
||||||
|
- ✓ 下载文件
|
||||||
|
- ✓ 生成签名 URL(临时下载链接)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 Supabase + MinIO
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 examples/test_https_storage.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用客户端
|
||||||
|
|
||||||
|
**Python**:
|
||||||
|
```python
|
||||||
|
from examples.storage_client import SupabaseStorageClient
|
||||||
|
|
||||||
|
client = SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-api-key'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript**:
|
||||||
|
```javascript
|
||||||
|
// 在浏览器中
|
||||||
|
const client = new SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-api-key'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 访问端点
|
||||||
|
|
||||||
|
- **Storage API**: `https://amiap.hzau.edu.cn/supa/storage/v1`
|
||||||
|
- **Dashboard**: `http://100.64.0.2:18000` (内网)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
supabase-stack/
|
||||||
|
├── 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 # 测试脚本
|
||||||
|
│
|
||||||
|
├── docker-compose.yml # Supabase 核心服务
|
||||||
|
├── docker-compose.s3.yml # MinIO 对象存储
|
||||||
|
├── .env # 环境变量配置
|
||||||
|
│
|
||||||
|
├── README.md # Supabase 原始说明
|
||||||
|
├── README_STORAGE.md # Storage 使用指南(本文档)
|
||||||
|
└── CHANGELOG.md # 版本变更记录
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 认证密钥
|
||||||
|
|
||||||
|
在 `.env` 文件中:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 服务密钥(后端使用,完全权限)
|
||||||
|
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
|
||||||
|
# 匿名密钥(前端使用,受权限控制)
|
||||||
|
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 服务管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看服务状态
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker compose logs -f storage
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
docker compose restart storage
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker compose stop
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 故障排查
|
||||||
|
|
||||||
|
查看 [运维指南](docs/OPERATIONS_GUIDE.md) 的故障排查章节。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 更多信息
|
||||||
|
|
||||||
|
- **完整文档**: 查看 `docs/` 目录
|
||||||
|
- **代码示例**: 查看 `examples/` 目录
|
||||||
|
- **原始 README**: [README.md](README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 功能特性
|
||||||
|
|
||||||
|
- ✅ 公网 HTTPS 访问
|
||||||
|
- ✅ 子路径支持(无需子域名)
|
||||||
|
- ✅ 文件上传/下载
|
||||||
|
- ✅ 生成临时下载链接(签名 URL)
|
||||||
|
- ✅ Bucket 管理
|
||||||
|
- ✅ 文件列表/移动/复制/删除
|
||||||
|
- ✅ Python 和 JavaScript 客户端
|
||||||
|
- ✅ 完整的 REST API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 使用场景
|
||||||
|
|
||||||
|
1. **Web 应用文件上传** - 用户头像、附件等
|
||||||
|
2. **临时文件分享** - 生成带过期时间的下载链接
|
||||||
|
3. **静态资源托管** - 图片、视频、文档
|
||||||
|
4. **移动应用存储** - 跨平台数据同步
|
||||||
|
5. **数据备份** - 自动化备份脚本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
如有问题,请查看:
|
||||||
|
1. [快速入门](docs/QUICK_START.md) - 基础使用
|
||||||
|
2. [运维指南](docs/OPERATIONS_GUIDE.md) - 深入了解
|
||||||
|
3. 测试脚本 - `python3 examples/test_https_storage.py`
|
||||||
|
|
||||||
|
🎉 **开始使用 Supabase Storage!**
|
||||||
293
supabase-stack/docs/DEPLOYMENT_INFO.md
Normal file
293
supabase-stack/docs/DEPLOYMENT_INFO.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Supabase 部署信息汇总
|
||||||
|
|
||||||
|
## 🌐 访问地址
|
||||||
|
|
||||||
|
### HTTPS 域名访问(推荐用于 API)
|
||||||
|
```
|
||||||
|
API Base URL: https://amiap.hzau.edu.cn/supa
|
||||||
|
```
|
||||||
|
|
||||||
|
### 内网访问(用于 Dashboard)
|
||||||
|
```
|
||||||
|
Dashboard: http://100.64.0.2:18000
|
||||||
|
API Base: http://100.64.0.2:18000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 认证信息
|
||||||
|
|
||||||
|
### Dashboard 登录
|
||||||
|
```
|
||||||
|
URL: http://100.64.0.2:18000
|
||||||
|
用户名: lab-admin
|
||||||
|
密码: 017b7076cfb25bd18410d1e5f4f7ec5a
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Keys(用于前端/后端开发)
|
||||||
|
```javascript
|
||||||
|
// ✅ 公开密钥 - 可以在前端使用
|
||||||
|
ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg"
|
||||||
|
|
||||||
|
// ⚠️ 私密密钥 - 仅后端使用,不要暴露
|
||||||
|
SERVICE_ROLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NjM4MDI2NjksImV4cCI6MjA3OTE2MjY2OX0.gQWUaTkZ6mjjlv2TED0cODp2meqqWuCGKZR1ptIbovg"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库连接
|
||||||
|
```
|
||||||
|
主机: 100.64.0.2 或 db (容器内)
|
||||||
|
端口: 5432
|
||||||
|
数据库: postgres
|
||||||
|
用户: postgres
|
||||||
|
密码: a837234b952ad7aa9ab4852f47021660c038209a71fce027cec8bab37ad82ae5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 API 端点
|
||||||
|
|
||||||
|
### ✅ 可用的服务端点
|
||||||
|
|
||||||
|
| 服务 | HTTPS 端点 | 内网端点 | 用途 |
|
||||||
|
|------|-----------|----------|------|
|
||||||
|
| REST API | `https://amiap.hzau.edu.cn/supa/rest/v1/` | `http://100.64.0.2:18000/rest/v1/` | 数据库 CRUD 操作 |
|
||||||
|
| Auth API | `https://amiap.hzau.edu.cn/supa/auth/v1/` | `http://100.64.0.2:18000/auth/v1/` | 用户认证 |
|
||||||
|
| Storage API | `https://amiap.hzau.edu.cn/supa/storage/v1/` | `http://100.64.0.2:18000/storage/v1/` | 文件存储 |
|
||||||
|
| Realtime | `wss://amiap.hzau.edu.cn/supa/realtime/v1/websocket` | `ws://100.64.0.2:18000/realtime/v1/websocket` | 实时订阅 |
|
||||||
|
| Dashboard | ❌ 不可用 | `http://100.64.0.2:18000/` | 管理界面 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 Docker 服务状态
|
||||||
|
|
||||||
|
### 核心服务
|
||||||
|
```bash
|
||||||
|
# 查看所有服务状态
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# 核心服务列表
|
||||||
|
supabase-db # PostgreSQL 数据库
|
||||||
|
supabase-kong # API 网关(端口: 18000)
|
||||||
|
supabase-auth # 用户认证服务
|
||||||
|
supabase-rest # REST API 服务
|
||||||
|
supabase-storage # 文件存储服务
|
||||||
|
supabase-realtime # 实时订阅服务
|
||||||
|
supabase-studio # Dashboard
|
||||||
|
supabase-meta # 数据库元数据
|
||||||
|
supabase-analytics # 日志分析
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常用命令
|
||||||
|
```bash
|
||||||
|
# 进入 supabase-stack 目录
|
||||||
|
cd /vol1/1000/docker_server/traefik/supabase-stack
|
||||||
|
|
||||||
|
# 启动所有服务
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 停止所有服务
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# 查看服务日志
|
||||||
|
docker compose logs -f kong
|
||||||
|
docker compose logs -f rest
|
||||||
|
docker compose logs -f auth
|
||||||
|
|
||||||
|
# 重启特定服务
|
||||||
|
docker compose restart kong
|
||||||
|
docker compose restart rest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 配置文件
|
||||||
|
|
||||||
|
### 重要文件位置
|
||||||
|
```
|
||||||
|
/vol1/1000/docker_server/traefik/supabase-stack/
|
||||||
|
├── .env # 环境变量配置
|
||||||
|
├── docker-compose.yml # Docker Compose 配置
|
||||||
|
├── VUE_API_INTEGRATION.md # Vue 集成文档(已创建)
|
||||||
|
├── DEPLOYMENT_INFO.md # 本文件
|
||||||
|
├── volumes/
|
||||||
|
│ ├── api/kong.yml # Kong 网关配置
|
||||||
|
│ ├── db/ # 数据库数据目录
|
||||||
|
│ └── storage/ # 文件存储目录
|
||||||
|
```
|
||||||
|
|
||||||
|
### .env 关键配置
|
||||||
|
```bash
|
||||||
|
# 数据库
|
||||||
|
POSTGRES_PASSWORD=a837234b952ad7aa9ab4852f47021660c038209a71fce027cec8bab37ad82ae5
|
||||||
|
POSTGRES_HOST=db
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=7a264228051ad10724934342ce62dce584161c248841061de0d0a56e92d9bb1a
|
||||||
|
|
||||||
|
# API Keys
|
||||||
|
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
DASHBOARD_USERNAME=lab-admin
|
||||||
|
DASHBOARD_PASSWORD=017b7076cfb25bd18410d1e5f4f7ec5a
|
||||||
|
|
||||||
|
# 域名配置
|
||||||
|
SITE_URL=https://amiap.hzau.edu.cn
|
||||||
|
API_EXTERNAL_URL=https://amiap.hzau.edu.cn/supa
|
||||||
|
SUPABASE_PUBLIC_URL=https://amiap.hzau.edu.cn/supa
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始(前端集成)
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
```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. 使用示例
|
||||||
|
```javascript
|
||||||
|
// 查询数据
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.select('*')
|
||||||
|
|
||||||
|
// 插入数据
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.insert([{ name: 'John', email: 'john@example.com' }])
|
||||||
|
|
||||||
|
// 用户登录
|
||||||
|
const { data, error } = await supabase.auth.signInWithPassword({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'password'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 测试 API
|
||||||
|
|
||||||
|
### 快速测试脚本
|
||||||
|
```bash
|
||||||
|
cd /vol1/1000/docker_server/traefik/supabase-stack
|
||||||
|
|
||||||
|
# 测试 REST API
|
||||||
|
curl -H "apikey: ANON_KEY" \
|
||||||
|
-H "Authorization: Bearer ANON_KEY" \
|
||||||
|
https://amiap.hzau.edu.cn/supa/rest/v1/
|
||||||
|
|
||||||
|
# 测试 Auth API
|
||||||
|
curl https://amiap.hzau.edu.cn/supa/auth/v1/health
|
||||||
|
|
||||||
|
# 测试 Storage API
|
||||||
|
curl https://amiap.hzau.edu.cn/supa/storage/v1/version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在浏览器中测试
|
||||||
|
```
|
||||||
|
1. 访问 Dashboard: http://100.64.0.2:18000
|
||||||
|
2. 使用用户名/密码登录
|
||||||
|
3. 创建测试表
|
||||||
|
4. 在 Vue 应用中调用 API
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 已知问题
|
||||||
|
|
||||||
|
### 1. Dashboard 子路径问题
|
||||||
|
- **问题**: `https://amiap.hzau.edu.cn/supa` 无法访问 Dashboard
|
||||||
|
- **原因**: Supabase Studio 是 SPA,不支持子路径部署
|
||||||
|
- **解决**: 使用内网地址 `http://100.64.0.2:18000`
|
||||||
|
|
||||||
|
### 2. Pooler 服务重启
|
||||||
|
- **问题**: `supabase-pooler` 一直重启
|
||||||
|
- **影响**: 无影响,这是连接池优化组件,非必需
|
||||||
|
- **解决**: 可忽略或停止该服务 `docker stop supabase-pooler`
|
||||||
|
|
||||||
|
### 3. REST API 缓存重试
|
||||||
|
- **问题**: 首次访问可能看到 "schema cache retrying"
|
||||||
|
- **原因**: PostgreSQL schema 缓存加载
|
||||||
|
- **解决**: 等待几秒自动恢复,或重启 `docker compose restart rest`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 开发文档
|
||||||
|
|
||||||
|
| 文档 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Vue 集成指南 | `VUE_API_INTEGRATION.md` | 详细的前端集成教程 |
|
||||||
|
| 部署信息 | `DEPLOYMENT_INFO.md` | 本文件 |
|
||||||
|
| 官方文档 | https://supabase.com/docs | Supabase 官方文档 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 故障排查
|
||||||
|
|
||||||
|
### 服务无法启动
|
||||||
|
```bash
|
||||||
|
# 查看日志
|
||||||
|
docker compose logs kong
|
||||||
|
docker compose logs rest
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
docker compose restart
|
||||||
|
|
||||||
|
# 完全重建
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 无法访问
|
||||||
|
```bash
|
||||||
|
# 检查 Kong 状态
|
||||||
|
docker compose ps kong
|
||||||
|
|
||||||
|
# 检查端口
|
||||||
|
netstat -tlnp | grep 18000
|
||||||
|
|
||||||
|
# 测试内网连接
|
||||||
|
curl http://100.64.0.2:18000/rest/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库连接问题
|
||||||
|
```bash
|
||||||
|
# 进入数据库容器
|
||||||
|
docker exec -it supabase-db psql -U postgres
|
||||||
|
|
||||||
|
# 查看数据库
|
||||||
|
\l
|
||||||
|
|
||||||
|
# 查看 schema
|
||||||
|
\dn
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 支持
|
||||||
|
|
||||||
|
如有问题,请检查:
|
||||||
|
1. 所有容器是否正常运行: `docker compose ps`
|
||||||
|
2. 查看服务日志: `docker compose logs [service]`
|
||||||
|
3. 参考 `VUE_API_INTEGRATION.md` 了解详细用法
|
||||||
|
4. 访问 Dashboard 管理数据库: `http://100.64.0.2:18000`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2025-11-22
|
||||||
|
**版本**: Supabase Self-Hosted Stack
|
||||||
350
supabase-stack/docs/OPERATIONS_GUIDE.md
Normal file
350
supabase-stack/docs/OPERATIONS_GUIDE.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# Supabase Stack 运维指南
|
||||||
|
|
||||||
|
完整的 Supabase 对象存储运维手册
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 目录
|
||||||
|
|
||||||
|
1. [系统架构](#系统架构)
|
||||||
|
2. [服务启动与管理](#服务启动与管理)
|
||||||
|
3. [Storage API 使用](#storage-api-使用)
|
||||||
|
4. [故障排查](#故障排查)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 系统架构
|
||||||
|
|
||||||
|
### 组件说明
|
||||||
|
|
||||||
|
```
|
||||||
|
外部用户 (HTTPS)
|
||||||
|
↓
|
||||||
|
Traefik (:443)
|
||||||
|
↓
|
||||||
|
Kong Gateway (:18000)
|
||||||
|
↓
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ Supabase Services │
|
||||||
|
├────────────────────────────────┤
|
||||||
|
│ • Auth (GoTrue) - 用户认证 │
|
||||||
|
│ • REST (PostgREST) - 数据API │
|
||||||
|
│ • Storage - 对象存储 │
|
||||||
|
│ • Realtime - 实时订阅 │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
PostgreSQL + MinIO
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问地址
|
||||||
|
|
||||||
|
| 服务 | 地址 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| 公网 API | https://amiap.hzau.edu.cn/supa | 所有 API 入口 |
|
||||||
|
| Storage API | https://amiap.hzau.edu.cn/supa/storage/v1 | 对象存储 |
|
||||||
|
| Dashboard | http://100.64.0.2:18000 | 管理后台(内网) |
|
||||||
|
|
||||||
|
### 数据存储
|
||||||
|
|
||||||
|
- **PostgreSQL 数据**: `./volumes/db/data/`
|
||||||
|
- **对象存储数据**: `/vol1/1000/s3/stub/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 服务启动与管理
|
||||||
|
|
||||||
|
### 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /vol1/1000/docker_server/traefik/supabase-stack
|
||||||
|
|
||||||
|
# 启动所有服务(包含 MinIO 对象存储后端)
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- `docker-compose.yml` - Supabase 核心服务
|
||||||
|
- `docker-compose.s3.yml` - MinIO 存储后端(**必需**)
|
||||||
|
|
||||||
|
### 查看状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看所有容器
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker compose logs -f storage
|
||||||
|
docker compose logs -f kong
|
||||||
|
```
|
||||||
|
|
||||||
|
### 停止/重启
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止服务
|
||||||
|
docker compose stop
|
||||||
|
|
||||||
|
# 重启特定服务
|
||||||
|
docker compose restart storage
|
||||||
|
docker compose restart minio
|
||||||
|
|
||||||
|
# 完全停止并删除容器(数据保留)
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
关键密钥在 `.env` 文件中:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 服务密钥(后端使用)
|
||||||
|
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NjM4MDI2NjksImV4cCI6MjA3OTE2MjY2OX0.gQWUaTkZ6mjjlv2TED0cODp2meqqWuCGKZR1ptIbovg
|
||||||
|
|
||||||
|
# 匿名密钥(前端使用)
|
||||||
|
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzYzODAyNjY5LCJleHAiOjIwNzkxNjI2Njl9.ltGXvQKpguLaf8Vzomn310hLgOZbrjqZT-F3rR00ulg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Storage API 使用
|
||||||
|
|
||||||
|
### API 端点
|
||||||
|
|
||||||
|
```
|
||||||
|
Base URL: https://amiap.hzau.edu.cn/supa
|
||||||
|
Storage: https://amiap.hzau.edu.cn/supa/storage/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整 Python 示例
|
||||||
|
|
||||||
|
保存为 `storage_client.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class StorageClient:
|
||||||
|
def __init__(self, base_url, api_key):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.headers = {
|
||||||
|
'apikey': api_key,
|
||||||
|
'Authorization': f'Bearer {api_key}'
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_bucket(self, name, public=False):
|
||||||
|
"""创建 bucket"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/bucket',
|
||||||
|
headers=self.headers,
|
||||||
|
json={'name': name, 'public': public}
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
def upload_file(self, bucket, file_path, storage_path=None):
|
||||||
|
"""上传文件"""
|
||||||
|
if not storage_path:
|
||||||
|
storage_path = Path(file_path).name
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers,
|
||||||
|
files={'file': f}
|
||||||
|
)
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
def download_file(self, bucket, storage_path, local_path):
|
||||||
|
"""下载文件"""
|
||||||
|
response = requests.get(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
if response.ok:
|
||||||
|
with open(local_path, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_signed_url(self, bucket, storage_path, expires_in=3600):
|
||||||
|
"""生成临时下载链接"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/sign/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers,
|
||||||
|
json={'expiresIn': expires_in}
|
||||||
|
)
|
||||||
|
if response.ok:
|
||||||
|
return self.base_url + response.json()['signedURL']
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
if __name__ == '__main__':
|
||||||
|
client = StorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建 bucket
|
||||||
|
client.create_bucket('my-bucket')
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
client.upload_file('my-bucket', 'photo.jpg', 'uploads/photo.jpg')
|
||||||
|
|
||||||
|
# 生成临时链接
|
||||||
|
url = client.create_signed_url('my-bucket', 'uploads/photo.jpg')
|
||||||
|
print(f'下载链接: {url}')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整 JavaScript 示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class StorageClient {
|
||||||
|
constructor(baseUrl, apiKey) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.headers = {
|
||||||
|
'apikey': apiKey,
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createBucket(name, isPublic = false) {
|
||||||
|
const response = await fetch(`${this.baseUrl}/storage/v1/bucket`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, public: isPublic })
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile(bucket, file, storagePath) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers,
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSignedUrl(bucket, storagePath, expiresIn = 3600) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/sign/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ expiresIn })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return this.baseUrl + data.signedURL;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
const client = new StorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
|
fileInput.addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
await client.uploadFile('my-bucket', file, `uploads/${file.name}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
完整代码示例请查看:`storage_client.py` 和 `storage_client.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 故障排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
**1. 服务无法启动**
|
||||||
|
```bash
|
||||||
|
# 检查端口占用
|
||||||
|
docker compose ps
|
||||||
|
netstat -tulpn | grep -E ":(8000|5432|9000)"
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker compose logs storage
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. API 返回 403**
|
||||||
|
```bash
|
||||||
|
# 检查密钥是否正确
|
||||||
|
grep SERVICE_ROLE_KEY .env
|
||||||
|
|
||||||
|
# 测试 API
|
||||||
|
curl -I https://amiap.hzau.edu.cn/supa/storage/v1/bucket \
|
||||||
|
-H "apikey: YOUR_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 上传失败**
|
||||||
|
```bash
|
||||||
|
# 检查 MinIO 是否运行
|
||||||
|
docker compose ps minio
|
||||||
|
|
||||||
|
# 查看 Storage 日志
|
||||||
|
docker compose logs storage | tail -50
|
||||||
|
```
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 快速测试脚本
|
||||||
|
python3 test_https_storage.py
|
||||||
|
|
||||||
|
# 手动测试
|
||||||
|
curl https://amiap.hzau.edu.cn/supa/rest/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 备份
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份数据库
|
||||||
|
docker exec supabase-db pg_dump -U postgres > backup.sql
|
||||||
|
|
||||||
|
# 备份对象存储
|
||||||
|
tar -czf s3_backup.tar.gz /vol1/1000/s3/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- `storage_client.py` - Python 完整客户端代码
|
||||||
|
- `storage_client.js` - JavaScript 完整客户端代码
|
||||||
|
- `test_https_storage.py` - 测试脚本
|
||||||
|
- `VUE_API_INTEGRATION.md` - Vue 集成指南
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 启动服务
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||||
|
|
||||||
|
# 2. 测试
|
||||||
|
python3 test_https_storage.py
|
||||||
|
|
||||||
|
# 3. 查看文档
|
||||||
|
cat storage_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
完成!现在可以开始使用 Supabase Storage 了。
|
||||||
76
supabase-stack/docs/QUICK_START.md
Normal file
76
supabase-stack/docs/QUICK_START.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Supabase Stack 快速参考
|
||||||
|
|
||||||
|
## 🚀 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /vol1/1000/docker_server/traefik/supabase-stack
|
||||||
|
|
||||||
|
# 启动所有服务(包含 MinIO 对象存储)
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||||
|
|
||||||
|
# 查看状态
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker compose logs -f storage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Storage API 访问
|
||||||
|
|
||||||
|
**端点**: `https://amiap.hzau.edu.cn/supa/storage/v1`
|
||||||
|
|
||||||
|
**密钥**:
|
||||||
|
```bash
|
||||||
|
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NjM4MDI2NjksImV4cCI6MjA3OTE2MjY2OX0.gQWUaTkZ6mjjlv2TED0cODp2meqqWuCGKZR1ptIbovg
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 快速使用
|
||||||
|
|
||||||
|
### Python
|
||||||
|
```python
|
||||||
|
from storage_client import SupabaseStorageClient
|
||||||
|
|
||||||
|
client = SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
client.upload_file('bucket', 'photo.jpg', 'uploads/photo.jpg')
|
||||||
|
|
||||||
|
# 生成临时链接
|
||||||
|
url = client.create_signed_url('bucket', 'uploads/photo.jpg', 3600)
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
```javascript
|
||||||
|
const client = new SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
await client.uploadFile('bucket', fileObject, 'uploads/photo.jpg');
|
||||||
|
|
||||||
|
// 生成临时链接
|
||||||
|
const url = await client.createSignedUrl('bucket', 'uploads/photo.jpg', 3600);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 文档
|
||||||
|
|
||||||
|
- **OPERATIONS_GUIDE.md** - 完整运维指南(必读)
|
||||||
|
- **storage_client.py** - Python 完整客户端代码
|
||||||
|
- **storage_client.js** - JavaScript 完整客户端代码
|
||||||
|
- **test_https_storage.py** - 测试脚本
|
||||||
|
|
||||||
|
## 🧪 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_https_storage.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 重要
|
||||||
|
|
||||||
|
- **MinIO 必须运行**:docker-compose.s3.yml 不能删除
|
||||||
|
- **数据位置**:/vol1/1000/s3/stub/
|
||||||
|
- **不要直接访问 MinIO**:统一通过 Storage API
|
||||||
1239
supabase-stack/docs/VUE_API_INTEGRATION.md
Normal file
1239
supabase-stack/docs/VUE_API_INTEGRATION.md
Normal file
File diff suppressed because it is too large
Load Diff
56
supabase-stack/docs/versions.md
Normal file
56
supabase-stack/docs/versions.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Docker Image Versions
|
||||||
|
|
||||||
|
## 2025-11-12
|
||||||
|
- supabase/studio:2025.11.10-sha-5291fe3 (prev 2025.10.27-sha-85b84e0)
|
||||||
|
- supabase/gotrue:v2.182.1 (prev v2.180.0)
|
||||||
|
- supabase/realtime:v2.63.0 (prev v2.57.2)
|
||||||
|
- supabase/storage-api:v1.29.0 (prev v1.28.2)
|
||||||
|
- supabase/edge-runtime:v1.69.23 (prev v1.69.15)
|
||||||
|
- supabase/supavisor:2.7.4 (prev 2.7.3)
|
||||||
|
|
||||||
|
## 2025-10-28
|
||||||
|
- supabase/studio:2025.10.27-sha-85b84e0 (prev 2025.10.20-sha-5005fc6)
|
||||||
|
- supabase/realtime:v2.57.2 (prev v2.56.0)
|
||||||
|
- supabase/storage-api:v1.28.2 (prev v1.28.1)
|
||||||
|
- supabase/postgres-meta:v0.93.1 (prev v0.93.0)
|
||||||
|
- supabase/edge-runtime:v1.69.15 (prev v1.69.14)
|
||||||
|
|
||||||
|
## 2025-10-21
|
||||||
|
- supabase/studio:2025.10.20-sha-5005fc6 (prev 2025.10.01-sha-8460121)
|
||||||
|
- supabase/realtime:v2.56.0 (prev v2.51.11)
|
||||||
|
- supabase/storage-api:v1.28.1 (prev v1.28.0)
|
||||||
|
- supabase/postgres-meta:v0.93.0 (prev v0.91.6)
|
||||||
|
- supabase/edge-runtime:v1.69.14 (prev v1.69.6)
|
||||||
|
- supabase/supavisor:2.7.3 (prev 2.7.0)
|
||||||
|
|
||||||
|
## 2025-10-13
|
||||||
|
- supabase/logflare:1.22.6 (prev 1.22.4)
|
||||||
|
|
||||||
|
## 2025-10-08
|
||||||
|
- supabase/studio:2025.10.01-sha-8460121 (prev 2025.06.30-sha-6f5982d)
|
||||||
|
- supabase/gotrue:v2.180.0 (prev v2.177.0)
|
||||||
|
- postgrest/postgrest:v13.0.7 (prev v12.2.12)
|
||||||
|
- supabase/realtime:v2.51.11 (prev v2.34.47)
|
||||||
|
- supabase/storage-api:v1.28.0 (prev v1.25.7)
|
||||||
|
- supabase/postgres-meta:v0.91.6 (prev v0.91.0)
|
||||||
|
- supabase/logflare:1.22.4 (prev 1.14.2)
|
||||||
|
- supabase/postgres:15.8.1.085 (prev 15.8.1.060)
|
||||||
|
- supabase/supavisor:2.7.0 (prev 2.5.7)
|
||||||
|
|
||||||
|
## 2025-07-15
|
||||||
|
- supabase/gotrue:v2.177.0 (prev v2.176.1)
|
||||||
|
- supabase/storage-api:v1.25.7 (prev v1.24.7)
|
||||||
|
- supabase/postgres-meta:v0.91.0 (prev v0.89.3)
|
||||||
|
- supabase/supavisor:2.5.7 (prev 2.5.6)
|
||||||
|
|
||||||
|
## 2025-07-02
|
||||||
|
- supabase/studio:2025.06.30-sha-6f5982d (prev 2025.06.02-sha-8f2993d)
|
||||||
|
- supabase/gotrue:v2.176.1 (prev v2.174.0)
|
||||||
|
- supabase/storage-api:v1.24.7 (prev v1.23.0)
|
||||||
|
- supabase/supavisor:2.5.6 (prev 2.5.1)
|
||||||
|
|
||||||
|
## 2025-06-03
|
||||||
|
- supabase/studio:2025.06.02-sha-8f2993d (prev 2025.05.19-sha-3487831)
|
||||||
|
- supabase/gotrue:v2.174.0 (prev v2.172.1)
|
||||||
|
- supabase/storage-api:v1.23.0 (prev v1.22.17)
|
||||||
|
- supabase/postgres-meta:v0.89.3 (prev v0.89.0)
|
||||||
467
supabase-stack/examples/storage_client.js
Normal file
467
supabase-stack/examples/storage_client.js
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
/**
|
||||||
|
* Supabase Storage REST API 完整客户端
|
||||||
|
* 无需 SDK,纯 JavaScript/TypeScript 实现
|
||||||
|
*
|
||||||
|
* 使用示例:
|
||||||
|
*
|
||||||
|
* const client = new SupabaseStorageClient(
|
||||||
|
* 'https://amiap.hzau.edu.cn/supa',
|
||||||
|
* 'your-service-role-key'
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // 创建 bucket
|
||||||
|
* await client.createBucket('my-bucket');
|
||||||
|
*
|
||||||
|
* // 上传文件
|
||||||
|
* await client.uploadFile('my-bucket', fileObject, 'uploads/photo.jpg');
|
||||||
|
*
|
||||||
|
* // 生成临时下载链接
|
||||||
|
* const url = await client.createSignedUrl('my-bucket', 'uploads/photo.jpg', 3600);
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SupabaseStorageClient {
|
||||||
|
/**
|
||||||
|
* 初始化客户端
|
||||||
|
* @param {string} baseUrl - Supabase Base URL
|
||||||
|
* @param {string} apiKey - SERVICE_ROLE_KEY 或 ANON_KEY
|
||||||
|
*/
|
||||||
|
constructor(baseUrl, apiKey) {
|
||||||
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||||
|
this.headers = {
|
||||||
|
'apikey': apiKey,
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Bucket 管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有 buckets
|
||||||
|
* @returns {Promise<Array>}
|
||||||
|
*/
|
||||||
|
async listBuckets() {
|
||||||
|
const response = await fetch(`${this.baseUrl}/storage/v1/bucket`, {
|
||||||
|
headers: this.headers
|
||||||
|
});
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 bucket
|
||||||
|
* @param {string} name - bucket 名称
|
||||||
|
* @param {boolean} isPublic - 是否公开
|
||||||
|
* @param {number} fileSizeLimit - 文件大小限制(字节)
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async createBucket(name, isPublic = false, fileSizeLimit = 52428800) {
|
||||||
|
const response = await fetch(`${this.baseUrl}/storage/v1/bucket`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
public: isPublic,
|
||||||
|
file_size_limit: fileSizeLimit
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return response.ok || response.status === 409;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 bucket
|
||||||
|
* @param {string} name - bucket 名称
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async deleteBucket(name) {
|
||||||
|
const response = await fetch(`${this.baseUrl}/storage/v1/bucket/${name}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.headers
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空 bucket
|
||||||
|
* @param {string} name - bucket 名称
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async emptyBucket(name) {
|
||||||
|
const response = await fetch(`${this.baseUrl}/storage/v1/bucket/${name}/empty`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 文件上传 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {File} file - 文件对象
|
||||||
|
* @param {string} storagePath - 云端存储路径
|
||||||
|
* @returns {Promise<Object|null>}
|
||||||
|
*/
|
||||||
|
async uploadFile(bucket, file, storagePath) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers,
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传字节数据
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {Blob|ArrayBuffer} data - 数据
|
||||||
|
* @param {string} storagePath - 云端存储路径
|
||||||
|
* @param {string} contentType - MIME 类型
|
||||||
|
* @returns {Promise<Object|null>}
|
||||||
|
*/
|
||||||
|
async uploadBytes(bucket, data, storagePath, contentType = 'application/octet-stream') {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': contentType
|
||||||
|
},
|
||||||
|
body: data
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {File} file - 文件对象
|
||||||
|
* @param {string} storagePath - 云端存储路径
|
||||||
|
* @returns {Promise<Object|null>}
|
||||||
|
*/
|
||||||
|
async updateFile(bucket, file, storagePath) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: this.headers,
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 文件下载 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件为 Blob
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} storagePath - 云端文件路径
|
||||||
|
* @returns {Promise<Blob|null>}
|
||||||
|
*/
|
||||||
|
async downloadFile(bucket, storagePath) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{ headers: this.headers }
|
||||||
|
);
|
||||||
|
return response.ok ? await response.blob() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件为 ArrayBuffer
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} storagePath - 云端文件路径
|
||||||
|
* @returns {Promise<ArrayBuffer|null>}
|
||||||
|
*/
|
||||||
|
async downloadBytes(bucket, storagePath) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{ headers: this.headers }
|
||||||
|
);
|
||||||
|
return response.ok ? await response.arrayBuffer() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件为文本
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} storagePath - 云端文件路径
|
||||||
|
* @returns {Promise<string|null>}
|
||||||
|
*/
|
||||||
|
async downloadText(bucket, storagePath) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{ headers: this.headers }
|
||||||
|
);
|
||||||
|
return response.ok ? await response.text() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 文件列表 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} folder - 文件夹路径
|
||||||
|
* @param {number} limit - 返回数量限制
|
||||||
|
* @param {number} offset - 偏移量
|
||||||
|
* @returns {Promise<Array|null>}
|
||||||
|
*/
|
||||||
|
async listFiles(bucket, folder = '', limit = 100, offset = 0) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/list/${bucket}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
prefix: folder,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
sortBy: { column: 'name', order: 'asc' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} searchText - 搜索关键词
|
||||||
|
* @param {number} limit - 返回数量限制
|
||||||
|
* @returns {Promise<Array|null>}
|
||||||
|
*/
|
||||||
|
async searchFiles(bucket, searchText, limit = 100) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/list/${bucket}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ search: searchText, limit })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 文件操作 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移动文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} fromPath - 源路径
|
||||||
|
* @param {string} toPath - 目标路径
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async moveFile(bucket, fromPath, toPath) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/move`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
bucketId: bucket,
|
||||||
|
sourceKey: fromPath,
|
||||||
|
destinationKey: toPath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} fromPath - 源路径
|
||||||
|
* @param {string} toPath - 目标路径
|
||||||
|
* @returns {Promise<Object|null>}
|
||||||
|
*/
|
||||||
|
async copyFile(bucket, fromPath, toPath) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/copy`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
bucketId: bucket,
|
||||||
|
sourceKey: fromPath,
|
||||||
|
destinationKey: toPath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok ? await response.json() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} storagePath - 文件路径
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async deleteFile(bucket, storagePath) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.headers
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除文件
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {Array<string>} filePaths - 文件路径数组
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async deleteFiles(bucket, filePaths) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/${bucket}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ prefixes: filePaths })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== URL 生成 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成签名 URL(临时下载链接)
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} storagePath - 文件路径
|
||||||
|
* @param {number} expiresIn - 过期时间(秒),默认 1 小时
|
||||||
|
* @returns {Promise<string|null>}
|
||||||
|
*/
|
||||||
|
async createSignedUrl(bucket, storagePath, expiresIn = 3600) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/sign/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ expiresIn })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// 签名 URL 需要加上 /storage/v1 前缀
|
||||||
|
return this.baseUrl + '/storage/v1' + data.signedURL;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成上传签名 URL(允许前端直接上传)
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} storagePath - 文件路径
|
||||||
|
* @returns {Promise<Object|null>} {url, token}
|
||||||
|
*/
|
||||||
|
async createUploadSignedUrl(bucket, storagePath) {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.baseUrl}/storage/v1/object/upload/sign/${bucket}/${storagePath}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.headers
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
url: this.baseUrl + '/storage/v1' + data.url,
|
||||||
|
token: data.token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取公开 URL(仅适用于 public bucket)
|
||||||
|
* @param {string} bucket - bucket 名称
|
||||||
|
* @param {string} storagePath - 文件路径
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getPublicUrl(bucket, storagePath) {
|
||||||
|
return `${this.baseUrl}/storage/v1/object/public/${bucket}/${storagePath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 使用示例 ====================
|
||||||
|
|
||||||
|
// 在浏览器中使用
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.SupabaseStorageClient = SupabaseStorageClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 Node.js 中使用
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = SupabaseStorageClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例代码
|
||||||
|
/*
|
||||||
|
// 初始化客户端
|
||||||
|
const client = new SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 上传文件(HTML5 File API)
|
||||||
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
|
fileInput.addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const result = await client.uploadFile('my-bucket', file, `uploads/${file.name}`);
|
||||||
|
console.log('上传成功:', result);
|
||||||
|
|
||||||
|
// 生成临时下载链接
|
||||||
|
const url = await client.createSignedUrl('my-bucket', `uploads/${file.name}`, 3600);
|
||||||
|
console.log('下载链接:', url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 列出文件
|
||||||
|
const files = await client.listFiles('my-bucket', 'uploads/');
|
||||||
|
console.log('文件列表:', files);
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const blob = await client.downloadFile('my-bucket', 'uploads/photo.jpg');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
// 创建下载链接
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'photo.jpg';
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
查看 OPERATIONS_GUIDE.md 了解更多用法
|
||||||
|
*/
|
||||||
349
supabase-stack/examples/storage_client.py
Normal file
349
supabase-stack/examples/storage_client.py
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Supabase Storage REST API 完整客户端
|
||||||
|
无需 SDK,仅使用 requests 库
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
class SupabaseStorageClient:
|
||||||
|
"""
|
||||||
|
Supabase Storage REST API 客户端
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
client = SupabaseStorageClient(
|
||||||
|
'https://amiap.hzau.edu.cn/supa',
|
||||||
|
'your-service-role-key'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建 bucket
|
||||||
|
client.create_bucket('my-bucket')
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
client.upload_file('my-bucket', 'photo.jpg', 'uploads/photo.jpg')
|
||||||
|
|
||||||
|
# 生成临时下载链接
|
||||||
|
url = client.create_signed_url('my-bucket', 'uploads/photo.jpg', 3600)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, api_key: str):
|
||||||
|
"""
|
||||||
|
初始化客户端
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: Supabase Base URL (如 https://amiap.hzau.edu.cn/supa)
|
||||||
|
api_key: SERVICE_ROLE_KEY 或 ANON_KEY
|
||||||
|
"""
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.headers = {
|
||||||
|
'apikey': api_key,
|
||||||
|
'Authorization': f'Bearer {api_key}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== Bucket 管理 ====================
|
||||||
|
|
||||||
|
def list_buckets(self) -> Optional[List[Dict]]:
|
||||||
|
"""列出所有 buckets"""
|
||||||
|
response = requests.get(
|
||||||
|
f'{self.base_url}/storage/v1/bucket',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
def create_bucket(self, name: str, public: bool = False,
|
||||||
|
file_size_limit: int = 52428800) -> bool:
|
||||||
|
"""
|
||||||
|
创建 bucket
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: bucket 名称
|
||||||
|
public: 是否公开
|
||||||
|
file_size_limit: 文件大小限制(字节)
|
||||||
|
"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/bucket',
|
||||||
|
headers=self.headers,
|
||||||
|
json={
|
||||||
|
'name': name,
|
||||||
|
'public': public,
|
||||||
|
'file_size_limit': file_size_limit
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.ok or response.status_code == 409
|
||||||
|
|
||||||
|
def delete_bucket(self, name: str) -> bool:
|
||||||
|
"""删除 bucket"""
|
||||||
|
response = requests.delete(
|
||||||
|
f'{self.base_url}/storage/v1/bucket/{name}',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
def empty_bucket(self, name: str) -> bool:
|
||||||
|
"""清空 bucket"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/bucket/{name}/empty',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
# ==================== 文件上传 ====================
|
||||||
|
|
||||||
|
def upload_file(self, bucket: str, file_path: str,
|
||||||
|
storage_path: Optional[str] = None) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
上传文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bucket: bucket 名称
|
||||||
|
file_path: 本地文件路径
|
||||||
|
storage_path: 云端存储路径(默认使用文件名)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'Key': 'path', 'Id': 'uuid'} 或 None
|
||||||
|
"""
|
||||||
|
if not storage_path:
|
||||||
|
storage_path = Path(file_path).name
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers,
|
||||||
|
files={'file': f}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
def upload_bytes(self, bucket: str, data: bytes, storage_path: str,
|
||||||
|
content_type: str = 'application/octet-stream') -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
上传字节数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bucket: bucket 名称
|
||||||
|
data: 字节数据
|
||||||
|
storage_path: 云端存储路径
|
||||||
|
content_type: MIME 类型
|
||||||
|
"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers={**self.headers, 'Content-Type': content_type},
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
def update_file(self, bucket: str, file_path: str, storage_path: str) -> Optional[Dict]:
|
||||||
|
"""更新已存在的文件"""
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
response = requests.put(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers,
|
||||||
|
files={'file': f}
|
||||||
|
)
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
# ==================== 文件下载 ====================
|
||||||
|
|
||||||
|
def download_file(self, bucket: str, storage_path: str, local_path: str) -> bool:
|
||||||
|
"""
|
||||||
|
下载文件到本地
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bucket: bucket 名称
|
||||||
|
storage_path: 云端文件路径
|
||||||
|
local_path: 本地保存路径
|
||||||
|
"""
|
||||||
|
response = requests.get(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
with open(local_path, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def download_bytes(self, bucket: str, storage_path: str) -> Optional[bytes]:
|
||||||
|
"""下载文件为字节数据"""
|
||||||
|
response = requests.get(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
return response.content if response.ok else None
|
||||||
|
|
||||||
|
# ==================== 文件列表 ====================
|
||||||
|
|
||||||
|
def list_files(self, bucket: str, folder: str = '',
|
||||||
|
limit: int = 100, offset: int = 0) -> Optional[List[Dict]]:
|
||||||
|
"""
|
||||||
|
列出文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bucket: bucket 名称
|
||||||
|
folder: 文件夹路径
|
||||||
|
limit: 返回数量限制
|
||||||
|
offset: 偏移量
|
||||||
|
"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/list/{bucket}',
|
||||||
|
headers=self.headers,
|
||||||
|
json={
|
||||||
|
'prefix': folder,
|
||||||
|
'limit': limit,
|
||||||
|
'offset': offset,
|
||||||
|
'sortBy': {'column': 'name', 'order': 'asc'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
def search_files(self, bucket: str, search_text: str, limit: int = 100) -> Optional[List[Dict]]:
|
||||||
|
"""搜索文件"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/list/{bucket}',
|
||||||
|
headers=self.headers,
|
||||||
|
json={'search': search_text, 'limit': limit}
|
||||||
|
)
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
# ==================== 文件操作 ====================
|
||||||
|
|
||||||
|
def move_file(self, bucket: str, from_path: str, to_path: str) -> bool:
|
||||||
|
"""移动文件"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/move',
|
||||||
|
headers=self.headers,
|
||||||
|
json={
|
||||||
|
'bucketId': bucket,
|
||||||
|
'sourceKey': from_path,
|
||||||
|
'destinationKey': to_path
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
def copy_file(self, bucket: str, from_path: str, to_path: str) -> Optional[Dict]:
|
||||||
|
"""复制文件"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/copy',
|
||||||
|
headers=self.headers,
|
||||||
|
json={
|
||||||
|
'bucketId': bucket,
|
||||||
|
'sourceKey': from_path,
|
||||||
|
'destinationKey': to_path
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response.json() if response.ok else None
|
||||||
|
|
||||||
|
def delete_file(self, bucket: str, storage_path: str) -> bool:
|
||||||
|
"""删除文件"""
|
||||||
|
response = requests.delete(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
def delete_files(self, bucket: str, file_paths: List[str]) -> bool:
|
||||||
|
"""批量删除文件"""
|
||||||
|
response = requests.delete(
|
||||||
|
f'{self.base_url}/storage/v1/object/{bucket}',
|
||||||
|
headers=self.headers,
|
||||||
|
json={'prefixes': file_paths}
|
||||||
|
)
|
||||||
|
return response.ok
|
||||||
|
|
||||||
|
# ==================== URL 生成 ====================
|
||||||
|
|
||||||
|
def create_signed_url(self, bucket: str, storage_path: str,
|
||||||
|
expires_in: int = 3600) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
生成签名 URL(临时下载链接)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bucket: bucket 名称
|
||||||
|
storage_path: 文件路径
|
||||||
|
expires_in: 过期时间(秒),默认 1 小时
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
完整的签名 URL
|
||||||
|
"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/sign/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers,
|
||||||
|
json={'expiresIn': expires_in}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
data = response.json()
|
||||||
|
# 签名 URL 需要加上 /storage/v1 前缀
|
||||||
|
return self.base_url + '/storage/v1' + data['signedURL']
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_upload_signed_url(self, bucket: str, storage_path: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
生成上传签名 URL(允许前端直接上传)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'url': upload_url, 'token': token}
|
||||||
|
"""
|
||||||
|
response = requests.post(
|
||||||
|
f'{self.base_url}/storage/v1/object/upload/sign/{bucket}/{storage_path}',
|
||||||
|
headers=self.headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
data = response.json()
|
||||||
|
return {
|
||||||
|
'url': self.base_url + '/storage/v1' + data['url'],
|
||||||
|
'token': data['token']
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_public_url(self, bucket: str, storage_path: str) -> str:
|
||||||
|
"""获取公开 URL(仅适用于 public bucket)"""
|
||||||
|
return f'{self.base_url}/storage/v1/object/public/{bucket}/{storage_path}'
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 使用示例 ====================
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 初始化客户端
|
||||||
|
client = SupabaseStorageClient(
|
||||||
|
base_url='https://amiap.hzau.edu.cn/supa',
|
||||||
|
api_key='your-service-role-key'
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Supabase Storage 客户端示例")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 1. 创建 bucket
|
||||||
|
print("\n1. 创建 bucket...")
|
||||||
|
client.create_bucket('test-bucket', public=False)
|
||||||
|
|
||||||
|
# 2. 列出 buckets
|
||||||
|
print("\n2. 列出所有 buckets...")
|
||||||
|
buckets = client.list_buckets()
|
||||||
|
if buckets:
|
||||||
|
for bucket in buckets:
|
||||||
|
print(f" - {bucket['name']}")
|
||||||
|
|
||||||
|
# 3. 上传文件
|
||||||
|
print("\n3. 上传文件...")
|
||||||
|
result = client.upload_bytes(
|
||||||
|
'test-bucket',
|
||||||
|
b'Hello from Supabase Storage!',
|
||||||
|
'test/hello.txt',
|
||||||
|
'text/plain'
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
print(f" ✓ 上传成功: {result['Key']}")
|
||||||
|
|
||||||
|
# 4. 生成临时下载链接
|
||||||
|
print("\n4. 生成临时下载链接...")
|
||||||
|
url = client.create_signed_url('test-bucket', 'test/hello.txt', 3600)
|
||||||
|
if url:
|
||||||
|
print(f" ✓ URL (1小时有效): {url[:80]}...")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("示例完成!查看 OPERATIONS_GUIDE.md 了解更多用法")
|
||||||
194
supabase-stack/examples/test_https_storage.py
Executable file
194
supabase-stack/examples/test_https_storage.py
Executable file
@@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
测试公网 HTTPS Supabase Storage API
|
||||||
|
快速验证所有功能是否正常
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BASE_URL = 'https://amiap.hzau.edu.cn/supa'
|
||||||
|
API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NjM4MDI2NjksImV4cCI6MjA3OTE2MjY2OX0.gQWUaTkZ6mjjlv2TED0cODp2meqqWuCGKZR1ptIbovg'
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'apikey': API_KEY,
|
||||||
|
'Authorization': f'Bearer {API_KEY}'
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print(" Supabase Storage 公网 HTTPS 功能测试")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
print(f"端点: {BASE_URL}/storage/v1")
|
||||||
|
print()
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
total_tests = 5
|
||||||
|
|
||||||
|
# 测试 1: 列出 Buckets
|
||||||
|
print("1️⃣ 列出 Buckets")
|
||||||
|
print("-" * 70)
|
||||||
|
try:
|
||||||
|
response = requests.get(f'{BASE_URL}/storage/v1/bucket', headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
buckets = response.json()
|
||||||
|
print(f"✓ 成功! 找到 {len(buckets)} 个 bucket")
|
||||||
|
for b in buckets:
|
||||||
|
print(f" - {b['name']}")
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
print(f"✗ 失败: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 错误: {e}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 测试 2: 创建 Bucket(或确认存在)
|
||||||
|
print("2️⃣ 确认测试 Bucket 存在")
|
||||||
|
print("-" * 70)
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f'{BASE_URL}/storage/v1/bucket',
|
||||||
|
headers=headers,
|
||||||
|
json={'name': 'test-https', 'public': False}
|
||||||
|
)
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
print("✓ 创建成功")
|
||||||
|
success_count += 1
|
||||||
|
elif response.status_code == 400 and 'already exists' in response.text.lower():
|
||||||
|
print("✓ Bucket 已存在(正常)")
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 响应: {response.status_code} - {response.text[:100]}")
|
||||||
|
# 检查是否能列出该 bucket(说明存在)
|
||||||
|
list_resp = requests.get(f'{BASE_URL}/storage/v1/bucket', headers=headers)
|
||||||
|
if list_resp.ok and any(b['name'] == 'test-https' for b in list_resp.json()):
|
||||||
|
print("✓ Bucket 已确认存在")
|
||||||
|
success_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 错误: {e}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 测试 3: 上传文件(如果存在则更新)
|
||||||
|
print("3️⃣ 上传/更新文件")
|
||||||
|
print("-" * 70)
|
||||||
|
test_data = b"Hello from HTTPS Storage API!\nThis is a test file.\nTimestamp: " + str(__import__('time').time()).encode()
|
||||||
|
try:
|
||||||
|
# 先尝试上传
|
||||||
|
response = requests.post(
|
||||||
|
f'{BASE_URL}/storage/v1/object/test-https/test.txt',
|
||||||
|
headers={**headers, 'Content-Type': 'text/plain'},
|
||||||
|
data=test_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
print("✓ 上传成功")
|
||||||
|
print(f" 响应: {response.json()}")
|
||||||
|
success_count += 1
|
||||||
|
elif response.status_code == 400 and 'already exists' in response.text.lower():
|
||||||
|
# 文件已存在,使用 PUT 更新
|
||||||
|
print(" 文件已存在,尝试更新...")
|
||||||
|
response = requests.put(
|
||||||
|
f'{BASE_URL}/storage/v1/object/test-https/test.txt',
|
||||||
|
headers={**headers, 'Content-Type': 'text/plain'},
|
||||||
|
data=test_data
|
||||||
|
)
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
print("✓ 更新成功")
|
||||||
|
print(f" 响应: {response.json()}")
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
print(f"✗ 更新失败: {response.status_code} - {response.text[:100]}")
|
||||||
|
else:
|
||||||
|
print(f"✗ 失败: {response.status_code} - {response.text[:100]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 错误: {e}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 测试 4: 下载文件
|
||||||
|
print("4️⃣ 下载文件")
|
||||||
|
print("-" * 70)
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f'{BASE_URL}/storage/v1/object/test-https/test.txt',
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
content = response.content.decode('utf-8')
|
||||||
|
print("✓ 下载成功")
|
||||||
|
print(f" 内容预览: {content[:50]}...")
|
||||||
|
print(f" 文件大小: {len(response.content)} bytes")
|
||||||
|
success_count += 1
|
||||||
|
else:
|
||||||
|
print(f"✗ 失败: {response.status_code}")
|
||||||
|
if response.text:
|
||||||
|
print(f" 错误信息: {response.text[:100]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 错误: {e}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 测试 5: 生成签名 URL
|
||||||
|
print("5️⃣ 生成签名 URL(临时下载链接)")
|
||||||
|
print("-" * 70)
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f'{BASE_URL}/storage/v1/object/sign/test-https/test.txt',
|
||||||
|
headers=headers,
|
||||||
|
json={'expiresIn': 3600}
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
# 签名 URL 需要加上 /storage/v1 前缀
|
||||||
|
signed_url = BASE_URL + '/storage/v1' + data['signedURL']
|
||||||
|
print("✓ 生成成功")
|
||||||
|
print(f" 完整 URL:")
|
||||||
|
print(f" {signed_url}")
|
||||||
|
print(f" 有效期: 1 小时")
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
# 验证签名 URL 是否可以下载(不带认证 headers,因为 token 在 URL 中)
|
||||||
|
print("\n 验证签名 URL 下载...")
|
||||||
|
try:
|
||||||
|
verify_response = requests.get(signed_url) # 不带 headers
|
||||||
|
if verify_response.status_code == 200:
|
||||||
|
content_preview = verify_response.content[:50].decode('utf-8', errors='ignore')
|
||||||
|
print(f" ✓ 签名 URL 可以正常下载!")
|
||||||
|
print(f" 大小: {len(verify_response.content)} bytes")
|
||||||
|
print(f" 内容: {content_preview}...")
|
||||||
|
print(f"\n 💡 这个 URL 可以直接在浏览器中打开或分享给他人")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ 签名 URL 返回: {verify_response.status_code}")
|
||||||
|
if verify_response.text:
|
||||||
|
print(f" 错误: {verify_response.text[:100]}")
|
||||||
|
except Exception as ve:
|
||||||
|
print(f" ⚠️ 验证失败: {ve}")
|
||||||
|
else:
|
||||||
|
print(f"✗ 失败: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ 错误: {e}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 总结
|
||||||
|
print("=" * 70)
|
||||||
|
print(f" 测试完成: {success_count}/{total_tests} 通过")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
if success_count == total_tests:
|
||||||
|
print("🎉 所有功能正常!")
|
||||||
|
print()
|
||||||
|
print("你现在可以:")
|
||||||
|
print(" ✓ 通过公网 HTTPS 上传文件")
|
||||||
|
print(" ✓ 下载文件")
|
||||||
|
print(" ✓ 生成临时下载链接")
|
||||||
|
print(" ✓ 管理 buckets")
|
||||||
|
print()
|
||||||
|
print("📚 查看完整文档:")
|
||||||
|
print(" - QUICK_START.md 快速入门")
|
||||||
|
print(" - OPERATIONS_GUIDE.md 运维指南")
|
||||||
|
print(" - storage_client.py Python 客户端")
|
||||||
|
print(" - storage_client.js JavaScript 客户端")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print(f"⚠️ {total_tests - success_count} 个测试失败")
|
||||||
|
print("请检查配置或查看文档")
|
||||||
|
sys.exit(1)
|
||||||
Reference in New Issue
Block a user