S3 对象存储
通过统一的 s3mini API 在本地存储、远端 S3 和平台实现之间切换。
@pluxel/plugin-storage目前只供 Pluxel 工作区使用,尚不是公开安装入口。完整边界见 Package 矩阵。
@pluxel/plugin-storage 提供接近原始 bucket 的 S3 能力。业务 Plugin 通过 S3.bucket(id).client 使用 s3mini 1.x API,宿主则通过 S3Plugin 配置一个有界的 local/remote bucket catalog。
写入与读取对象
import { } from '@pluxel/plugin-storage'
import { , } from '@pluxel/core'
@({ : 'Assets' })
export class extends {
constructor(private readonly : ) {
super()
}
(: string, : ) {
return this.
.('assets')
..(`avatars/${}.webp`, , 'image/webp')
}
(: string) {
return this..('assets')..(`avatars/${}.webp`)
}
}默认 local backend
没有配置时,S3Plugin 使用:
{
buckets: [
{
id: 'default',
backend: {
type: 'local',
rootDir: '.pluxel/s3',
bucketName: 'local',
syncWrites: true,
},
},
],
}显式配置示例:
以下 host 是 测试宿主,用于验证装配。应用入口按 添加插件 配置清单、配置记录和自动启动。
import { S3Plugin } from '@pluxel/plugin-storage'
await host.start(S3Plugin, {
initialConfig: {
buckets: [
{
id: 'assets',
backend: {
type: 'local',
rootDir: '/var/lib/my-app/s3',
bucketName: 'assets',
syncWrites: true,
},
},
],
},
})
await host.start(AssetsPlugin)local backend 支持 CRUD、typed/stream/range/conditional reads、delimiter/prefix listing、opaque pagination、copy/move、批量删除和显式 multipart upload。putAnyObject() 会流式写入临时 container,校验声明长度,计算 SHA-256 ETag,按需 fsync,最后 atomic rename;失败的写入不会发布成完整对象。
完整 S3 key 会被可逆 base32 编码并拆成有界文件名。../x、leading slash、反斜杠、空 path segment 和 Unicode 都只是普通 object key,不参与本地路径解析。
.pluxel/s3 适合开发。生产若使用 local backend,应把 rootDir 放在 immutable dist/ 之外,并纳入备份、磁盘容量、权限和故障恢复策略。
S3Client 是 Omit<S3mini, `_${string}`>,因此公开业务面直接锚定 s3mini:bucket create/exists、listing、字符串/JSON/ArrayBuffer/Response 读取、range 与 conditional read、PUT、multipart、copy/move、delete 和 presign 都使用原生 method signature。
它是 raw bucket capability,不会自动给 key 加 caller prefix。key namespace、metadata schema、对象大小与 content type policy、保留期和删除所有权都由 consumer 或 host contract 定义。
先调用 putAvatar() 写入一个小图片,再调用 getAvatar() 确认字节与 Content-Type。示例选择的 bucket ID 是 assets;默认配置只有 default,因此不能省略上面的显式 bucket 配置。
远端 S3:anonymous
同一个 provider 可以创建真实 S3mini client:
await host.start(S3Plugin, {
initialConfig: {
buckets: [
{
id: 'public-assets',
backend: {
type: 'remote',
endpoint: 'https://public-assets.s3.example.com',
region: 'auto',
credentials: { type: 'anonymous' },
requestSizeInBytes: 8 * 1024 * 1024,
requestAbortTimeout: 30_000,
minPartSize: 8 * 1024 * 1024,
},
},
],
},
})endpoint 必须是没有 userinfo、query 或 hash 的 HTTP(S) URL。minPartSize 至少 5 MiB、最多 5 GiB;其余 size/timeout 字段必须是正整数。
anonymous 是显式选择,不是 credential 查找失败后的隐式 fallback。
部署配置入口
根入口的 S3Config 就是 S3Plugin 的配置声明,可直接用于 envBinding(S3Plugin, { config: { schema: S3Config, mapping: { buckets: 'S3_BUCKETS' } } });S3_BUCKETS 是完整 bucket 数组 JSON。文件部署复用同一 schema。配置层优先级、数组替换和 env 锁定规则由 Host 统一维护。
无 Workbench 时 local/anonymous 直接启动,remote/vault 使用根入口的 S3VaultSchema 声明 部署凭据。Workbench rotation 只是可写 Vault 的可选管理入口;只读绑定的 credential 更新由部署输入负责。bucket 对象内容是业务存储,不作为 Plugin 配置恢复或覆盖。
远端 S3:Vault credential
access key 不进入普通 Plugin config。先把以下对象写入 Vault:
import type { S3AccessKeyCredentials } from '@pluxel/plugin-storage'
const credentials = {
accessKeyId: '…',
secretAccessKey: '…',
} satisfies S3AccessKeyCredentials然后在 S3 config 中只保存引用:
await host.start(S3Plugin, {
initialConfig: {
buckets: [
{
id: 'assets',
backend: {
type: 'remote',
endpoint: 'https://assets.s3.us-east-1.amazonaws.com',
region: 'us-east-1',
credentials: {
type: 'vault',
namespace: 'production-secrets',
key: 'assets.s3',
},
},
},
],
},
})省略 namespace 时使用当前 S3Plugin 的 Vault namespace;default bucket 的 key 默认是 s3.credentials,其他 bucket 默认是
s3.<bucket-id>.credentials。provider 在 init() 中为每个 remote/vault bucket 订阅 credential record,并校验初始 snapshot。Vault 不可用、key
缺失或对象非法分别以 S3CredentialsError.reason 的 unavailable、missing、invalid 失败整个 generation。
Workbench enabled 时,provider 固定发布一个 S3 buckets Content,以 bounded rows 显示各 ID 的 local、remote/anonymous 或
remote/vault;只有选中的 remote/vault bucket 接受一次性 password form,local/anonymous 报告 not-applicable、明确拒绝且
不访问 Vault。Handler 将 replacement access key 写入当前配置引用的 Vault record,
并重新检查 authenticated Management principal、当前 generation 与 backend,
串行执行写入和 flush()。Content 不读取或展示旧 credential,新 credential 也不会进入 plan、load、action result 或日志。
保存后新请求使用更新的 client;已发出的请求可继续使用旧 snapshot。record 缺失或非法时,后续 client 访问失败,不回退 anonymous。环境和文件绑定只读;修改绑定源后重启应用。
这条 Content 不能用于首次 provisioning。缺失或非法 record 会让 S3Plugin 启动失败,而失败 generation 的 Workbench publication 必然回滚;宿主必须在启动前写入 credential。不要为了显示 setup Content 让 S3 capability 半启动,也不要另建脱离 Plugin owner lifecycle 的 secret registry。
local 和 anonymous backend 不访问 Vault。当前 s3mini contract 不接受 session token;STS、平台 credential chain 或特殊认证协议需要由平台提供另一个 S3 implementation。
local 与 remote 的能力差异
remote backend 是真实 s3mini client,支持范围取决于目标 S3 服务。local backend 对无法保持语义的操作会明确抛出 S3UnsupportedOperationError,其稳定 code 为 S3_UNSUPPORTED_OPERATION,包括:
- presigned URL;
- bucket/object versioning 相关操作;
- SSE-C;
- version-specific copy/delete;
- replacement tagging。
不要通过捕获并忽略该错误来假装操作成功。若业务必须依赖 presign 或 versioning,应在部署契约中要求 remote/platform provider,或显式检测并返回 capability unavailable。
multipart 与大对象
普通 putAnyObject() 已支持流式 body。需要显式控制 multipart 时,使用 s3mini 的原生序列:
const client = this.s3.bucket('assets').client
const uploadId = await client.getMultipartUploadId(
'exports/archive.bin',
'application/octet-stream',
)
try {
const first = await client.uploadPart('exports/archive.bin', uploadId, firstChunk, 1)
const second = await client.uploadPart('exports/archive.bin', uploadId, secondChunk, 2)
await client.completeMultipartUpload('exports/archive.bin', uploadId, [first, second])
} catch (error) {
await client.abortMultipartUpload('exports/archive.bin', uploadId)
throw error
}firstChunk 与 secondChunk 应使用 uploadPart() 接受的 s3mini body 类型。part number、最小 part size 与服务限制仍由 s3mini/backend 约束。
Catalog 生命周期与一致性
S3Plugin 的 buckets 配置包含 1–64 个唯一 stable ID。consumer 通过 s3.bucket(id) 取得 owner-bound handle;重复选择为
O(1),consumer 或 provider 停止后旧 handle 会被撤销。所有 bucket 属于同一个原子 generation:资源并行初始化,任一项失败会回滚
全部项。需要独立启停、故障隔离或扩缩容的对象存储应使用独立进程/host。
provider stop/replacement 后,旧 S3 caller facade 先由 Core generation gate 拒绝。此前取得的 local client
会被 revoke 并抛 S3NotRunningError(code S3_NOT_RUNNING);remote client 的 in-flight fetch 会收到 lifecycle abort。
对象存储不是关系型事务。若数据库 metadata 与对象必须协调,先设计显式 state machine,再用 outbox、幂等 key 和补偿流程处理对象 side effect;不要假设 DB transaction 能回滚 S3 PUT。
将对象缺失返回给调用方
S3 继续提供 s3mini 原生接口;业务 consumer 才知道缺少对象是否是可恢复失败。getObject() 以 null
表示对象不存在,空文本是成功值。下面用 default bucket 读取文本文件:
import { , } from '@pluxel/core'
import { , } from '@pluxel/core/better-result'
import { } from '@pluxel/plugin-storage'
export class extends ('DocumentNotFound')<{ : string }> {}
@()
export class extends {
constructor(private readonly : ) {
super()
}
async (: string): <<string, >> {
const = await this..()..(`documents/${}.txt`)
return === null ? .(new ({ })) : .()
}
}调用方可对 DocumentNotFound 提供重新上传入口,成功时使用 value。不要捕获所有异常并返回缺失:bucket
未配置、认证/权限、网络/磁盘错误和 generation 撤回应继续拒绝。该示例只读取文本;流式 Response、multipart
或已经开始的写入仍遵循自己的 body、取消、补偿和部分成功协议,不能靠 Result 回滚。
最后更新于