Skip to content

Storage API ​

Storage API 为每个应用提供隔离的 S3 兼容对象空间。文件内容通过短期预签名 URL 在客户端与对象存储之间直传;StarEcho API 只负责权限、能力策略、上传会话、元数据、配额和审计,不代理大文件字节。

默认能力 ​

能力默认值
单次 PUT 上限1 GiB(按应用生效)
Multipart 对象上限5 GiB(按应用生效)
默认分片16 MiB,可配置为 8–64 MiB
最大分片数10,000
上传 URL 有效期1 小时,可配置为 60 秒–7 天
Multipart 会话24 小时,过期后自动中止
默认容量配额2 GiB
Range / HEAD支持

不要把这些数值写死在业务中。应用应先调用能力接口,平台会返回该应用实际生效的限制。降低单次 PUT 上限可由应用 Owner 直接生效;提高上限需要管理员审核。降低不会删除已经存在的超限对象,只约束新上传。

认证与重放防护 ​

基础路径为 /storage/api/,支持:

  • Authorization: Basic base64(client_id:client_secret):只用于可信服务端和内部工具。
  • Authorization: Bearer <access_token>:用户 OIDC 身份。

client_secret 不能进入浏览器、游戏包或桌面客户端。Basic 和云函数内部调用以应用身份访问;Bearer 直调还会按操作检查应用角色权限:读取为 storage.read,上传/确认/Multipart 为 storage.upload,删除为 storage.delete。面向用户的上传入口应由已完成业务权限检查的云函数调用 $storage 签发短期 URL。

Storage API 请求还需携带:

请求头说明
X-TimestampUnix 秒时间戳,与服务器相差不超过 ±5 分钟
X-Nonce每次请求不同的 16–64 位字母数字字符串

预签名 URL 本身不添加这些头;它必须作为不透明字符串原样用于指定 HTTP Method,不能解码、重排参数、改写 Host 或替换域名。

查询能力与用量 ​

http
GET /storage/api/capabilities
GET /storage/api/usage

能力响应示例:

json
{
  "provider": "cloudflare-r2",
  "maxSingleObjectBytes": 1073741824,
  "multipartUploadSupported": true,
  "minPartBytes": 8388608,
  "maxPartBytes": 67108864,
  "defaultPartBytes": 16777216,
  "maxParts": 10000,
  "maxObjectBytes": 5368709120,
  "checksumAlgorithms": ["md5", "sha256"],
  "rangeDownloadSupported": true,
  "headSupported": true,
  "uploadUrlExpiresSeconds": 3600
}

用量响应会返回 quotaBytes、usedBytes、reservedBytes、remainingBytes、activeMultipartUploads 和 maxConcurrentMultipartUploads。未完成的有效上传会占用预留空间,避免并发签发超过配额。月度下载 URL 签发诊断和真实流量不可用标记见“审计、告警与保留策略”。

单次上传(兼容接口) ​

现有小文件无需迁移:

  1. POST /storage/api/upload 获取 PUT URL 和一次性确认令牌。
  2. 客户端直接 PUT 最终二进制 Body。
  3. POST /storage/api/confirm 回读对象存储实际元数据并确认。

获取 URL ​

http
POST /storage/api/upload
Content-Type: application/json

{
  "path": "releases/app.apk",
  "size": 253023298,
  "contentType": "application/vnd.android.package-archive",
  "sha256": "4ed50bf91e8c3c9e5e6a30ea4fcc4827aa91dd26c3d1e9e1b98ca524960b9d17",
  "metadata": { "versionName": "2026.07.29" },
  "allowOverwrite": false,
  "expiresIn": 3600,
  "bindContentLength": true
}

path 必须是 1–512 字符的相对对象路径,不能包含反斜杠、开头斜杠或 . / .. 路径段。size 是最终 HTTP Body 的字节数,不是字符串字符数。

旧调用不传 allowOverwrite 时继续保持历史上的可覆盖行为;新工具和新业务建议显式传 false,确需替换时再传 true。

响应包含 url、token、expiresIn、requestedSize、contentType、contentLengthBound、requiredHeaders,以及哈希的可信级别。新客户端建议传 bindContentLength: true,此时签名会绑定精确的 HTTP Body 字节数和 MIME Type,客户端必须发送 requiredHeaders 对应的值;浏览器会根据 Blob/File Body 自动生成 Content-Length,Node 客户端应显式设置。为兼容既有 UE 和 S3 HTTP 实现,旧调用不传该选项时仍不绑定这两个请求头。确认阶段一律通过 HeadObject 回读实际字节数和类型:调用方在 confirm 中显式传入 size/contentType 时,StarEcho 会拒绝与对象存储实际值不一致的对象;云函数与旧客户端不传时,直接采用对象存储实际值记录元数据与配额。

单次上传先写入不可枚举的临时物理 Key;confirm 校验成功后,平台在对象存储内部复制到没有 PUT 凭证的版本 Key,再原子更新 storage_file.path 的逻辑映射并回收临时对象。因此旧上传 URL 在确认后即使尚未自然过期,重放也只能改写未发布的临时 Key,不能改变正式下载对象;覆盖上传也不会在确认前替换当前版本。服务启动及每小时任务会删除过期单次上传的 staging 对象和令牌并记录 upload_expire,provider 暂时失败时保留记录供下次重试。迁移前在途令牌、旧版确定性物理 Key 和既有逻辑路径继续兼容。

确认 ​

http
POST /storage/api/confirm
Content-Type: application/json

{
  "path": "releases/app.apk",
  "token": "opaque-one-time-token",
  "size": 253023298,
  "contentType": "application/vnd.android.package-archive"
}

成功响应包含:

json
{
  "path": "releases/app.apk",
  "size": 253023298,
  "contentType": "application/vnd.android.package-archive",
  "sha256": "4ed50b...",
  "sha256Verification": "declared",
  "sha256VerifyStatus": "pending",
  "etag": "...",
  "metadata": { "versionName": "2026.07.29" },
  "createdAt": "1785290000",
  "updatedAt": null
}

确认会使用 HeadObject 回读实际大小和类型(confirm 显式传入 size/contentType 时校验一致性,未传入时直接采用实际值)并记录 ETag。sha256Verification: "declared" 表示哈希由可信客户端或开发工具声明,但平台尚未重新读取整个对象;不要把 Multipart ETag 当作文件 SHA-256。默认 R2 对象会持久化进入限流后台队列,sha256VerifyStatus 依次为 pending、processing、verified 或 failed。平台流式回读每个字节且哈希匹配后,sha256Verification 才原子升级为 platform_verified。自定义 OSS 不会自动回读,以免平台替应用产生第三方下载流量费用。

应用所有者可对失败任务重新入队:

http
POST /storage/manage/{clientId}/files/verify-sha256
Authorization: Bearer <owner_access_token>
Content-Type: application/json

{ "path": "releases/app.apk" }

R2 的 Multipart SHA-256 是组合校验而不是 FULL_OBJECT SHA-256,因此平台验证不能由 Multipart ETag 或组合校验冒充。后台任务跨 PM2 实例使用数据库状态与全局互斥,每次只验证一个对象,失败最多自动尝试三次。

Multipart 与断点续传 ​

建议 100 MiB 以上文件使用 Multipart。会话对外只暴露 StarEcho uploadId,不会暴露底层 provider Upload ID。

1. 创建会话 ​

http
POST /storage/api/multipart/create

{
  "path": "website/releases/android/2026.07.29/xutry.apk",
  "fileSize": 253023298,
  "contentType": "application/vnd.android.package-archive",
  "sha256": "4ed50bf91e8c3c9e5e6a30ea4fcc4827aa91dd26c3d1e9e1b98ca524960b9d17",
  "metadata": { "versionName": "2026.07.29" },
  "partSize": 16777216,
  "allowOverwrite": false
}

响应包含 uploadId、partSize、partCount、expiresAt 和 expiresIn。

2. 批量获取分片 URL ​

http
POST /storage/api/multipart/urls

{
  "path": "website/releases/android/2026.07.29/xutry.apk",
  "uploadId": "opaque-starecho-upload-id",
  "partNumbers": [1, 2, 3, 4]
}

一次最多申请 100 个 URL。每项包含 partNumber、该分片要求的 size、method: "PUT"、requiredHeaders: { "Content-Length": size } 和 url。分片签名始终绑定精确长度;浏览器应上传对应的精确 Blob.slice(),Node 客户端应发送返回的 Content-Length。上传成功后必须保存响应 ETag;浏览器需要在应用 CORS 中暴露 ETag。

3. 查询已上传分片 ​

http
GET /storage/api/multipart/parts?path={path}&uploadId={uploadId}

返回 provider 已保存的 partNumber、etag、size 和总 uploadedBytes。断线恢复时只为缺失编号重新申请 URL。

4. 幂等完成 ​

http
POST /storage/api/multipart/complete

{
  "path": "website/releases/android/2026.07.29/xutry.apk",
  "uploadId": "opaque-starecho-upload-id",
  "parts": [
    { "partNumber": 1, "etag": "..." },
    { "partNumber": 2, "etag": "..." }
  ],
  "sha256": "4ed50bf91e8c3c9e5e6a30ea4fcc4827aa91dd26c3d1e9e1b98ca524960b9d17"
}

平台会对照 provider 列表验证连续编号、每片 ETag、非末片固定大小、末片精确大小和最终对象字节数。重复完成返回同一对象结果,不会产生第二份对象。provider 已完成但响应超时的会话会通过 HEAD 恢复数据库元数据。

5. 中止与列出会话 ​

http
POST /storage/api/multipart/abort
GET  /storage/api/multipart/uploads?includeTerminal=false

中止请求体为 { "path": "...", "uploadId": "..." }。主动中止和定时过期清理都会释放 provider 分片;已完成会话的中止按幂等完成返回。

对象元数据与下载 ​

http
GET /storage/api/head?path={path}
GET /storage/api/meta?path={path}

head 实时读取对象存储权威大小、MIME、ETag 和最后修改时间;meta 返回平台确认后的数据库记录。

下载接口兼容无选项调用,并支持响应覆盖:

http
GET /storage/api/download?path={path}&expiresIn=3600&downloadName=xuTry.apk&contentType=application%2Fvnd.android.package-archive&cacheControl=public%2C%20max-age%3D3600

响应同时返回:

  • url:绑定 GET 的临时 URL,支持标准 Range 请求和 206 Partial Content。
  • headUrl:绑定 HEAD 的临时 URL。SigV4 会绑定 HTTP Method,因此不要拿 GET URL 改成 HEAD。
  • size、contentType、etag、sha256、sha256Verification 和 sha256VerifyStatus。

下载字节不经过云函数。私有 R2 预签名 URL 必须保留 R2 S3 endpoint;不能把 Host 改成自定义域名。需要 CDN 自定义域名时,应为公开且不可变的发布路径单独配置 R2 Custom Domain 和缓存策略。

列出与删除 ​

http
GET    /storage/api/files?prefix=releases/&limit=50&page=1
DELETE /storage/api/file

删除请求体为 { "path": "releases/old.apk" }。列表只返回已确认对象;有效单次或 Multipart 会话对应的对象不会被孤儿清理误删。

浏览器 CORS ​

管理面板可按应用分别填写生产和开发 Origin,例如:

text
production: https://app.example.com
development: http://localhost:3000

只填写 Origin,不包含路径。协议、主机和端口共同组成 Origin,因此 http://localhost:3000 只允许该端口,并不代表所有 localhost 端口。平台基线只包含当前 StarEcho 部署在服务端配置中声明的站点 Origin:本地部署可通过 STARECHO_CORS_ORIGIN 配置平台自己的 localhost,生产部署不会默认放行应用的 localhost 或任意端口。应用所有者可在此处自行登记所需的精确开发 Origin,保存后由平台自动同步 R2,无需平台管理员手动修改桶规则。

平台默认共享桶会合并已审核应用的明确白名单,允许 GET、PUT、HEAD,允许 Content-Type、Content-Length、校验和及签名所需请求头,并暴露 ETag、Content-Length、Content-Range、Accept-Ranges 和校验和响应头。写入来源不使用 *;Origin 白名单也不会代替短期预签名凭证的路径和方法约束。

数据库迁移只增加 CORS 设置结构,不会替应用选择可信 Origin。部署匹配的新服务端后,应用所有者必须在 Storage 设置中保存上述生产/开发 Origin;保存会重新合并并下发默认桶 CORS,无需平台管理员登录 Cloudflare。设置响应中的 corsSync 为 applied 表示本次已下发,pending 表示设置已持久化但 provider 同步失败或正在由另一实例执行,控制台会保留提示且平台每小时自动重试;not_requested 表示这次没有修改 CORS 或存储类型。无效、带路径或缺少精确端口的 Origin 会返回 STORAGE_INVALID_CORS_ORIGIN,不会再被静默丢弃。

平台在 Storage 服务获批、应用删除、默认/自定义存储切换时立即重算,并每小时执行一次带 MySQL 命名锁的一致性同步,避免多实例并发覆盖或数据库与 R2 配置漂移。应在真实 APK 验收前确认两个 Origin 已出现在 provider 的预检响应中。

管理与审计接口 ​

  • PUT /storage/manage/{clientId}/settings:配置上传 URL 有效期、默认分片大小、1–32 个并发分片会话上限及生产/开发 CORS Origin。
  • POST /storage/manage/{clientId}/object-limit/request:调整单次 PUT 阈值或分片完成后的真正单文件上限;降低立即生效,提高进入管理员审批。
  • GET /storage/manage/{clientId}/object-limit/requests:分别读取两类上限的最近一次申请状态。
  • GET /storage/manage/{clientId}/operations:查询初始化、完成、终止、过期、删除和下载凭证签发事件。

上限申请请求使用同一个可扩展协议:

json
{
  "limitType": "multipart_object",
  "requestedBytes": 10737418240,
  "reason": "需要分发 10 GiB 的不可变发布包"
}

limitType 可为 single_put 或 multipart_object。为兼容已部署客户端,省略时仍表示 single_put。新应用默认单次 PUT 上限为 1 GiB、分片单文件上限为 5 GiB;管理接口的 adjustableLimits 返回当前可申请范围。单次 PUT 不能高于分片单文件上限。当前 64 MiB 最大分片与 10,000 分片策略形成约 625 GiB 的平台有效硬上限。降低只约束后续上传会话,不删除已有对象;提高经管理员批准后原子更新申请状态和应用 Storage 设置。单文件上限与应用总容量配额相互独立,二者都必须足够。

retentionRules 可配置最多 32 条 { id, prefix, expireAfterDays } 规则,期限为 1–3650 天;默认空数组,不自动删除。每日任务按逻辑路径前缀和对象最后更新时间选取过期对象,通过正常 Storage 删除流程同时更新数据库、配额、审计和未引用物理对象。平台没有直接下发共享桶 Lifecycle,因为那会绕过逻辑元数据并留下不可下载的脏记录。空 prefix 会匹配应用全部对象,应谨慎启用。

storagePrefix 只决定后续新对象的物理前缀;切换或清空时不会移动、删除或隐藏已经确认的对象,既有逻辑路径仍通过数据库记录的完整物理 Key 读取。空字符串表示恢复默认 clientId 前缀。

管理控制台以 usedBytes + reservedBytes 计算容量压力,在 80% 显示预警、95% 显示严重告警,并同时展示上传预留空间;服务端仍以权威配额检查决定是否接受新会话。

Storage 数据面审计记录应用、调用模式、调用者 UID、逻辑路径、大小、会话和时间,不保存完整预签名 URL、provider Upload ID 或永久凭据。getUsage() 的容量、预留空间和并发会话为平台权威值;它还返回当前 UTC 月的 monthlyDownloadUrlIssueCount、monthlyDownloadUrlIssuedObjectBytes 和 monthlyWindowStart 作为签发趋势诊断,并明确返回 monthlyTransferBytes: null、monthlyTransferMetering: "not_available"。R2 Operations Analytics 虽能按 objectName 统计请求次数和状态,但当前没有提供每次 GET/Range 的实际响应字节总量,因此共享桶内无法权威计算应用级真实月传输量;平台不会用“URL 签发大小”冒充实际流量。需要精确月流量限额时,必须增加位于下载数据路径上的 Worker/自定义域名日志计量层。

云函数 $storage ​

旧方法保持兼容,并新增能力、Multipart、HEAD 和下载选项:

javascript
const caps = await $storage.getCapabilities();
const usage = await $storage.getUsage();

const single = await $storage.getUploadUrl(path, size, contentType, {
  sha256,
  metadata,
  allowOverwrite: false,
  expiresIn: 3600,
});
await $storage.confirmUpload(path, single.token);

const upload = await $storage.createMultipartUpload(path, {
  fileSize: size,
  contentType,
  sha256,
  partSize: 16 * 1024 * 1024,
  allowOverwrite: false,
});
const urls = await $storage.getMultipartUploadUrls(
  path,
  upload.uploadId,
  [1, 2, 3],
);
const current = await $storage.listMultipartUploadParts(path, upload.uploadId);
const file = await $storage.completeMultipartUpload(path, upload.uploadId, {
  parts,
  sha256,
});
await $storage.abortMultipartUpload(path, upload.uploadId);

const authoritative = await $storage.head(path);
const download = await $storage.getDownloadUrl(path, {
  expiresIn: 3600,
  downloadName: "release.apk",
  cacheControl: "public, max-age=3600",
});

withBasic() 和 asUser() 返回相同方法集合。云函数桥接保留结构化错误的 code、retryable 和可选 retryAfterSeconds。

稳定错误协议 ​

错误保留旧的 error / error_description 顶层字段,同时新增:

json
{
  "error": "file_too_large",
  "code": "STORAGE_FILE_TOO_LARGE",
  "message": "Requested object exceeds the application limit.",
  "error_description": "Requested object exceeds the application limit.",
  "retryable": false,
  "maxBytes": 1073741824
}
code含义重试
STORAGE_FILE_TOO_LARGE超过应用对象上限否
STORAGE_QUOTA_EXCEEDED配额或预留空间不足配额调整后
STORAGE_UPLOAD_EXPIREDURL 或会话过期新建/重新签发
STORAGE_PART_MISMATCH分片编号、大小或 ETag 不一致修正后
STORAGE_CHECKSUM_MISMATCH声明哈希冲突重新上传
STORAGE_FORBIDDENBearer 用户缺少所需 Storage 角色权限否
STORAGE_OBJECT_NOT_FOUND对象或会话不存在否
STORAGE_RATE_LIMITED达到并发限制是
STORAGE_INVALID_CORS_ORIGINCORS Origin 不完整、含路径或端口不精确修正后
STORAGE_INVALID_RETENTION_RULE保留策略 ID、前缀或期限无效否
STORAGE_TEMPORARILY_UNAVAILABLE完成中或 provider 临时失败是

VS Code / Codex 工具 ​

扩展提供能力/用量查询、工作区内文件上传、对象读取/列表/删除、临时下载凭证、Multipart 列表/中止工具。starecho_upload_storage_file 会验证本地绝对路径位于选定工作区,计算或核对 SHA-256,自动选择单次或 Multipart,并将续传状态保存在 .starecho/storage-uploads/。starecho_create_storage_download_url 可通过 method: "GET" | "HEAD" 选择要复制的 method-bound 凭证,默认仍为 GET;工具结果不会回显完整预签名 URL 或永久凭据。平台错误会在 VS Code AI 和 MCP 中保留 code、HTTP 状态、retryable 与 retryAfterSeconds,调用者无需解析自然语言即可决定停止、重试或重新签发。

Provider 约束参考 ​

可先运行 node P:\Project_Web\StarEcho\scripts_test\storage-platform-local-e2e.cjs 做完全隔离的平台验收。该脚本使用临时 MySQL 8.4、MinIO、当前编译服务和需求文档中的真实 APK,覆盖迁移、Basic/Bearer 权限、ReplayGuard、浏览器 CORS 请求形态、旧单次上传、断点分片、并发会话限额、月度下载签发诊断、错误路径/长度/哈希、幂等完成、HEAD、Range、完整 SHA-256、前缀切换、过期 staging、审计和清理;容器通过随机名称和专用标签校验后删除,不连接生产。MinIO 不等同于 R2 桶级 CORS 实现,因此真实 Origin 白名单仍必须在部署后验收。

部署后可在受信任的运维终端使用仓库内 scripts_test/storage-production-e2e.cjs 执行真实 APK 验收。脚本要求通过环境变量显式提供 API、应用凭据、APK 路径和 STARECHO_E2E_ALLOW_WRITE=I_UNDERSTAND;它不会输出预签名 URL,默认使用隔离逻辑路径并在成功或失败后清理测试对象。若还设置 STARECHO_E2E_ANDROID_SERIAL 与 STARECHO_E2E_ALLOW_ANDROID_INSTALL=I_UNDERSTAND,脚本会将刚从 Storage 下载且通过完整 SHA-256 校验的 APK 安装到唯一指定、已授权的 ADB 设备,并核对 com.starecho.xutry;未同时提供这两个变量时绝不会修改 Android 设备。该脚本不在 package.json 中注册,也不会进入生产构建产物。

© 2026 StarEcho