InsForge Storage SDK 实战指南:用一行createClient完成对象存储的上传、下载与删除
【免费下载链接】InsForgeThe all-in-one, open-source backend platform for agentic coding. InsForge gives your coding agent database, auth, storage, compute, hosting, and AI gateway to ship full-stack apps end-to-end.项目地址: https://gitcode.com/GitHub_Trending/in/InsForge
InsForge 是一个开源的 all-in-one 后端平台,内置的对象存储(Storage)模块为你的应用提供 Bucket 管理、文件上传、下载、删除、列表与公开 URL 生成能力。本文以仓库存档文档(.archive/docs/deprecated/insforge-storage-sdk.md)为核心,结合当前仓库后端源码,系统讲解 InsForge Storage SDK 的完整用法:从客户端初始化、Bucket 与文件操作,到浏览器端上传/展示图片、错误处理的最佳实践。读完本文,你将能直接在项目里接入 InsForge 对象存储,并理解每个 SDK 调用在服务端对应的 REST 路由与权限行为。
快速开始:初始化客户端
Storage SDK 是 InsForge TypeScript SDK(@insforge/sdk)的存储子模块。第一步是创建客户端实例并指定后端地址:
import { createClient } from '@insforge/sdk'; const client = createClient({ baseUrl: 'http://localhost:7130' });baseUrl指向 InsForge 后端的对外地址。本地开发默认端口为7130(见示例中的http://localhost:7130),部署后请替换为你的实际域名或 IP。- 初始化后,通过
client.storage访问存储能力;与数据库、认证等模块一样,它是 SDK 中一个独立的能力域。
核心概念:Bucket 与 Object Key
在进入 API 之前,先明确两个贯穿全文的概念(与 S3 的对象存储模型一致):
- Bucket(存储桶):对象的命名空间容器,分为**公开桶(public)与私有桶(private)**两类。公开桶允许未认证的下载,私有桶的所有操作都需要认证。
- Object Key(对象键):对象在桶内的唯一标识,支持路径形式(如
'folder/subfolder/file.jpg')。在服务端,StorageService.validateKey会拒绝包含..(目录穿越)或以/开头的 key(见 storage.service.ts)。
from(bucketName):选择操作目标
所有文件操作都从选定一个 Bucket 开始:
const bucket = client.storage.from('avatars'); // Returns StorageBucket instancefrom('avatars')返回一个StorageBucket实例,后续的upload、download、remove、list、getPublicUrl都通过它调用。值得注意的是,Bucket 的创建与删除由管理端完成(源码中为verifyAdmin保护的管理接口),SDK 不负责建桶/删桶——原文档的 Notes 一节对此有明确说明。
文件操作 API 详解
upload:按指定 Key 上传
// Upload with specific key const { data, error } = await client.storage .from('avatars') .upload('user-123.jpg', file); // data: StorageFileSchema { bucket, key, size, mimeType, uploadedAt, url }返回的data遵循StorageFileSchema,其字段定义在仓库的 storage.schema.ts 中:
| 字段 | 类型 | 说明 |
|---|---|---|
key | string | 对象键 |
bucket | string | 所属桶名 |
size | number | 字节大小 |
mimeType | string(可选) | MIME 类型 |
uploadedAt | string | 上传时间(ISO 字符串) |
url | string | 可访问的下载 URL |
服务端实现中,upload对应PUT /api/storage/buckets/:bucketName/objects/*路由,语义是"在精确 Key 上创建或替换对象"(标准 PUT 语义,覆盖已有对象);SDK 内部负责构造 multipart/form-data 请求(见 index.routes.ts 中的dynamicUploadSingle('file')中间件与putObject的INSERT ... ON CONFLICT DO UPDATE)。
uploadAuto:自动生成唯一 Key
// Upload with auto-generated key const { data, error } = await client.storage .from('avatars') .uploadAuto(file); // Generated key format: filename-timestamp-random.extuploadAuto让服务端为你生成不会冲突的 Key,对应POST /api/storage/buckets/:bucketName/objects路由。服务端StorageService.generateObjectKey的实现(storage.service.ts)为:
generateObjectKey(originalFilename: string): string { const timestamp = Date.now(); const randomStr = Math.random().toString(36).substring(2, 8); const fileExt = originalFilename ? path.extname(originalFilename) : ''; const baseName = originalFilename ? path.basename(originalFilename, fileExt) : 'file'; const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '-').substring(0, 32); const objectKey = `${sanitizedBaseName}-${timestamp}-${randomStr}${fileExt}`; return objectKey; }可以看到 Key 由三部分组成:清洗后的文件名(最长 32 字符,非法字符替换为-)、毫秒级时间戳、6 位随机字符串,最后拼接原始扩展名。这正是原文档所说filename-timestamp-random.ext格式的精确来源。当多个用户并发上传同名文件时,uploadAuto可从根本上避免 Key 冲突(这也是原文档 Notes 中"UseuploadAuto()to prevent filename conflicts"的原因)。
download:下载对象为 Blob
const { data: blob, error } = await client.storage .from('avatars') .download('user-123.jpg'); // data: Blob (can convert to URL with URL.createObjectURL(blob))download返回浏览器Blob对象,配合URL.createObjectURL(blob)可以直接用于<img>、<video>或<a download>等场景。服务端对应GET /api/storage/buckets/:bucketName/objects/*路由:公开桶直接放行(conditionalDownloadAuth中间件跳过认证),私有桶要求 JWT 或 API Key 认证。
remove:删除对象
const { data, error } = await client.storage .from('avatars') .remove('user-123.jpg');对应服务端DELETE /api/storage/buckets/:bucketName/objects/*路由。服务端还支持批量删除(DELETE /objects,一次最多 1000 个 Key,见deleteObjectsRequestSchema约束),并会先删除数据库元数据行、再清理底层存储(S3/本地文件),对每个 Key 返回deleted/notFound/failed三种状态。
list:列出对象(支持前缀过滤与分页)
const { data, error } = await client.storage .from('avatars') .list({ prefix: 'users/', // Filter by prefix search: 'profile', // Search in filenames limit: 10, // Max results (default: 100) offset: 0 // Skip results }); // data: ListObjectsResponseSchema { bucketName, objects[], pagination }list对应服务端GET /api/storage/buckets/:bucketName/objects路由,其行为可以在服务端源码中精确验证:
prefix:按前缀过滤,服务端执行key LIKE 'escapedPrefix%',escapeSqlLikePattern会先转义%与_通配符(防 SQL 注入)。search:在文件名中做子串匹配,服务端执行key LIKE '%escapedQuery%'。limit:默认 100,服务端将入参钳制在[1, 1000]区间(Math.min(Math.max(1, limit), 1000))。offset:跳过前 N 条,服务端保证不小于 0。
响应的pagination结构定义在 storage-api.schema.ts 的listObjectsResponseSchema中,包含offset、limit、total三个字段(total由服务端COUNT(*)查询返回),便于前端实现"加载更多"或分页组件。注意当前 schema 的顶层字段实际为data(对象数组)与pagination,原文档中bucketName为早期版本形态,以仓库 schema 为准。
getPublicUrl:零请求生成公开 URL
// Get public URL (no API call) const url = client.storage .from('avatars') .getPublicUrl('user-123.jpg'); // Returns: http://localhost:7130/api/storage/buckets/avatars/objects/user-123.jpggetPublicUrl是纯本地拼接,不发起任何网络请求,因此适合公开桶对象的直接引用。其 URL 模式与服务端StorageService.buildObjectUrl保持一致:
const base = `${getApiBaseUrl()}/api/storage/buckets/${bucket}/objects/${encodeURIComponent(key)}`;服务端在返回StorageFileSchema时会追加?v=<version>缓存失效参数(基于 etag,回退到uploaded_at),CDN 按完整 URL 缓存时可实现"覆盖上传后立即看到新内容",无需调用失效 API。但手动调用getPublicUrl得到的裸 URL 不携带版本戳,覆盖上传后若命中 CDN 缓存可能看到旧版本。
上传实战:三种典型场景
从<input type="file">上传
// HTML: <input type="file" id="fileInput"> const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; const { data, error } = await client.storage .from('uploads') .upload(`photos/${file.name}`, file);利用 Key 支持路径的特性,可以很方便地把上传文件归入photos/子目录。
上传 Blob
const blob = new Blob(['Hello World'], { type: 'text/plain' }); const { data, error } = await client.storage .from('documents') .upload('hello.txt', blob);服务端会校验并规范化 MIME 类型:路由中的enforceSafeMimeType会读取文件魔数(magic bytes)来覆盖客户端上报的mimetype;对于可执行类型(HTML、SVG、JS)统一归一化为application/octet-stream,避免存储可被浏览器直接执行的资源(详见 mime-guard.ts 与路由中的isUnsafeMime/resolveSafeMimeType)。
使用自定义文件名
// Simple file upload - SDK handles FormData creation const file = fileInput.files[0]; const { data, error } = await client.storage .from('uploads') .upload('custom-name.jpg', file);SDK 负责 FormData 的构造,你只需要提供目标 Key 与文件对象。
下载与展示:浏览器中的正确姿势
下载并显示图片
const { data: blob, error } = await client.storage .from('avatars') .download('user-123.jpg'); if (blob) { const url = URL.createObjectURL(blob); document.getElementById('avatar').src = url; // Clean up URL.revokeObjectURL(url); }要点:用完createObjectURL生成的 URL 后,调用URL.revokeObjectURL(url)释放内存,避免长页面/频繁切换头像时内存泄漏。
公开桶:直接使用 URL
// For public buckets, use direct URL const url = client.storage .from('public-avatars') .getPublicUrl('user-123.jpg'); document.getElementById('avatar').src = url;对于公开桶,直接用getPublicUrl得到的 URL 赋给src即可,浏览器无需认证即可加载——服务端的conditionalDownloadAuth中间件在检测到公开桶时会跳过verifyUser认证(index.routes.ts)。
错误处理模式
SDK 的所有操作都返回{ data, error }解构形式,建议始终检查error:
const { data, error } = await client.storage .from('avatars') .upload('user-123.jpg', file); if (error) { if (error.statusCode === 409) { console.error('File already exists'); } else if (error.statusCode === 404) { console.error('Bucket not found'); } else { console.error(error.message); } }这两个状态码在服务端有明确对应逻辑:
- 409 Conflict(对象已存在):
putObject遇到主键冲突时(PostgreSQL 错误码23505),路由将其映射为409 STORAGE_ALREADY_EXISTS(见 index.routes.ts 的mapObjectWriteError)。注意upload使用PUT语义,上传到已存在 Key 时会覆盖;409 主要出现在并发写入或唯一性约束冲突场景。 - 404 Not Found(桶不存在):
POST /objects上传到不存在的桶时,服务端返回404 STORAGE_NOT_FOUND,错误信息会提示"Create the bucket first using POST /api/storage/buckets"。 - 其他错误通过
error.message携带具体原因,例如 key 非法(包含..或以/开头)返回 400、RLS 权限不足返回 403STORAGE_PERMISSION_DENIED、超过配置的最大上传大小返回 413(服务端confirmUpload会以配置的maxFileSizeMb钳制,默认上限见 storage-config.service.ts)。
权限模型与注意事项
原文档 Notes 中列出的行为,在服务端均有对应实现,这里汇总成一张速查表:
| 规则 | 服务端实现依据 |
|---|---|
| Bucket 创建/删除由管理端完成,SDK 不负责 | POST /buckets、DELETE /buckets均挂verifyAdmin中间件(index.routes.ts) |
| 公开桶允许未认证下载 | conditionalDownloadAuth对公开桶跳过verifyUser |
| 私有桶所有操作需要认证 | 其余路径统一走verifyUser,且对象级可见性由storage.objects表的 RLS 策略控制 |
| Key 支持路径形式 | list的prefix、上传时的folder/subfolder/file.jpg均由 Key 的LIKE匹配实现 |
用uploadAuto()避免文件名冲突 | generateObjectKey追加timestamp-random后缀,见上文源码 |
补充几点实战提醒:
- 可见性与 RLS:普通终端用户(JWT)对私有桶对象的访问受行级安全(RLS)策略约束,
storage.objects的默认策略按uploaded_by归属控制可见性;API Key 与项目管理员走后端池绕过 RLS。若你的应用需要"多人共享同一对象",需要在数据库中调整相应的 SELECT 策略。 - 不安全 MIME 强制下载:即使某个对象存储了
text/html等不安全 MIME,服务端在响应时会强制加Content-Disposition: attachment与X-Content-Type-Options: nosniff,浏览器只会下载而不会内联渲染,这是服务端的纵深防御(路由中isUnsafeMime判断)。 - 下载策略与预签名 URL:SDK 的
download在服务端可能被实现为"直接转发"或"重定向到预签名 URL"(getDownloadStrategy返回presigned/direct两种策略)。私有桶的预签名 URL 有效期默认 1 小时(PRIVATE_BUCKET_EXPIRY = 3600秒),公开桶不过期,且支持通过expiresIn参数自定义(钳制在 1 秒到 7 天之间)。 - 对象大小限制:上传大小受存储配置
maxFileSizeMb约束(1–200 MB,见updateStorageConfigRequestSchema),超过会收到 413 错误。
以上 API 行为均有对应的单元测试覆盖,例如 storage-routes.test.ts 验证了 Bucket 创建、对象 CRUD 与错误码映射;底层存储支持本地文件系统与 S3 两种 Provider(LocalStorageProvider/S3StorageProvider,在 storage.provider 下),因此同一套 SDK 接口在自托管与云环境间无缝切换。掌握本文的 SDK 用法与权限模型,你就能在 InsForge 上快速落地文件上传、头像、附件、媒体库等典型存储场景。
【免费下载链接】InsForgeThe all-in-one, open-source backend platform for agentic coding. InsForge gives your coding agent database, auth, storage, compute, hosting, and AI gateway to ship full-stack apps end-to-end.项目地址: https://gitcode.com/GitHub_Trending/in/InsForge
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考