Files
labweb/supabase-stack/examples/storage_client.js
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

468 lines
14 KiB
JavaScript
Raw Permalink 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 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 了解更多用法
*/