一个面向 SaaS 和内部平台的轻量对象存储服务,提供对象版本、内容寻址去重、租户配额、分片上传、预签名下载、软删除、审计和异步垃圾回收。
客户端
|
v
对象存储 API
|-- API Key 鉴权
|-- 租户配额
|-- 对象版本
|-- 分片上传
|-- ETag / 预签名下载
|
+--> SQLite WAL:元数据、引用计数、上传状态、回收任务
|
+--> Blob 文件系统:SHA-256 内容寻址
|
v
GC Worker
数据库保存对象元数据,文件系统保存内容。相同内容使用同一个 SHA-256 Blob,覆盖和删除只更新引用关系,不在请求线程中直接做危险的物理删除。
- 多租户 API Key 鉴权和对象隔离
- 对象上传、下载、列表和软删除
- 对象版本历史,覆盖写入不会丢失旧版本
- SHA-256 内容寻址和 Blob 去重
- 租户配额校验,超过配额时事务回滚
ETag条件下载,内容未变化时返回304- HMAC 预签名下载令牌和过期校验
- 分片上传、分片覆盖和完整性检查
- SQLite WAL 元数据持久化
- 无引用 Blob 的异步垃圾回收 Worker
- 租户级固定窗口限流、审计日志和 Prometheus 指标
- Docker Compose 分离 API 和 GC Worker
- Node.js 24+
- 或 Docker / Docker Compose
项目使用 Node.js 内置 HTTP、SQLite、Crypto 和文件系统模块,无第三方运行时依赖。
npm test
npm run test:load
npm start另开终端启动垃圾回收 Worker:
npm run worker服务默认监听 http://localhost:8800。
curl.exe -X POST http://localhost:8800/admin/tenants `
-H "Authorization: Bearer local-admin-token" `
-H "Content-Type: application/json" `
-d '{"name":"内容平台","quotaBytes":104857600}'响应中的 apiKey 只展示一次,应当安全保存。
curl.exe -X PUT http://localhost:8800/v1/objects/media/logo.txt `
-H "X-API-Key: obj_xxx" `
-H "Content-Type: text/plain" `
--data-binary "hello object storage"响应包含 etag 和对象 version。
curl.exe http://localhost:8800/v1/objects/media/logo.txt `
-H "X-API-Key: obj_xxx"使用上次响应的 ETag:
curl.exe http://localhost:8800/v1/objects/media/logo.txt `
-H "X-API-Key: obj_xxx" `
-H 'If-None-Match: "sha256..."'对象未变化时返回 304 Not Modified。
curl.exe -X POST http://localhost:8800/v1/presigned-downloads `
-H "X-API-Key: obj_xxx" `
-H "Content-Type: application/json" `
-d '{"bucket":"media","objectKey":"logo.txt","expiresIn":300}'调用 /download?token=... 时不需要 API Key,但令牌包含租户、对象版本和过期时间,并使用 HMAC 校验。
curl.exe -X POST http://localhost:8800/v1/uploads `
-H "X-API-Key: obj_xxx" `
-H "Content-Type: application/json" `
-d '{"bucket":"media","objectKey":"large.bin","contentType":"application/octet-stream","totalParts":2}'
curl.exe -X PUT http://localhost:8800/v1/uploads/UPLOAD_ID/parts/1 `
-H "X-API-Key: obj_xxx" `
--data-binary "part one"
curl.exe -X PUT http://localhost:8800/v1/uploads/UPLOAD_ID/parts/2 `
-H "X-API-Key: obj_xxx" `
--data-binary "part two"
curl.exe -X POST http://localhost:8800/v1/uploads/UPLOAD_ID/complete `
-H "X-API-Key: obj_xxx"| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/health |
健康检查 |
GET |
/metrics |
Prometheus 指标 |
POST |
/admin/tenants |
创建租户,需要管理员令牌 |
GET |
/v1/tenant |
查询当前租户配额 |
PUT |
/v1/objects/:bucket/*key |
上传对象并创建新版本 |
GET |
/v1/objects/:bucket/*key |
下载当前或指定版本对象 |
GET |
/v1/objects |
按桶和前缀列出当前对象 |
GET |
/v1/objects/:bucket/versions?key=... |
查询对象版本 |
DELETE |
/v1/objects/:bucket/*key |
软删除对象 |
POST |
/v1/presigned-downloads |
创建预签名下载令牌 |
POST |
/v1/uploads |
创建分片上传 |
PUT |
/v1/uploads/:id/parts/:part |
上传分片 |
POST |
/v1/uploads/:id/complete |
合并并提交分片上传 |
GET |
/download?token=... |
使用预签名令牌下载 |
覆盖上传会产生新版本,历史版本仍可通过 ?version=N 下载。软删除只隐藏当前版本并释放租户逻辑配额,但历史版本仍然保留,因此其 Blob 不会立即物理删除。只有 Blob 没有任何历史版本引用时,GC Worker 才会执行回收。
| 环境变量 | 默认值 | 说明 |
|---|---|---|
PORT |
8800 |
API 端口 |
DATABASE_PATH |
data/objects.db |
SQLite 文件 |
STORAGE_ROOT |
data |
Blob 和临时分片目录 |
ADMIN_TOKEN |
local-admin-token |
管理员令牌 |
SIGNING_SECRET |
ADMIN_TOKEN |
预签名令牌密钥 |
TENANT_QUOTA_BYTES |
52428800 |
租户默认配额 |
RATE_LIMIT_PER_MINUTE |
600 |
每租户每分钟请求数 |
MAX_OBJECT_BYTES |
52428800 |
单对象最大大小 |
MAX_PART_BYTES |
10485760 |
单分片最大大小 |
GC_POLL_MS |
1000 |
GC Worker 轮询间隔 |
GC_LEASE_MS |
30000 |
GC 任务租约时间 |
GC_GRACE_MS |
60000 |
Blob 回收宽限期 |
docker compose up --build生产环境必须修改 ADMIN_TOKEN 和 SIGNING_SECRET,通过 HTTPS 提供服务,并将本地文件系统替换为 S3、MinIO 或其他可靠对象存储。
npm test
npm run test:load测试覆盖对象版本、ETag、预签名下载、软删除、配额、分片上传、租户隔离、限流和 GC。负载脚本并发上传 200 个对象并输出吞吐。
详细设计见 架构文档。