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