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:
zly
2025-11-22 21:03:00 +08:00
parent 8b068d8171
commit 9fa602f21b
9 changed files with 3261 additions and 0 deletions

View 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 了解更多用法
*/