/** * 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} */ 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} filePaths - 文件路径数组 * @returns {Promise} */ 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} */ 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} {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 了解更多用法 */