将 0.12 存储桶挂载迁移到 S3Mount,它会挂载相同的存储桶前缀,且容器中没有存储桶凭据。Worker 中的 S3Gateway 为每个存储请求签名。在 0.12 中,mountBucket() 将凭据放入容器,或通过 R2 绑定挂载。
你的 1.0 类已替换 0.12 Sandbox 类,并具有 替换 Sandbox 类 中的 container getter、ensureRunning() 和 startContainer() 方法,以及 ENV 常量。其镜像包含 S3Mount 使用的 sandbox-shim。S3Mount 通过 this.ctx.exports 到达 S3Gateway,这需要 compatibility date 2025-11-17 或更晚,或 enable_ctx_exports 标志。
本页替换 mountBucket()、unmountBucket()、它们的选项以及它们的错误。要在新项目中构建相同挂载,请参阅 挂载 R2 存储桶。
-
将 FUSE 和
s3fs添加到Dockerfile:Dockerfiledockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates fuse3 git python3 s3fs \ && rm -rf /var/lib/apt/lists/* -
从 Worker 的主模块导出
S3Gateway:src/index.tsts import { S3Mount, SandboxS3MountError, } from "@cloudflare/sandbox"; // S3Mount sends each storage request to this entrypoint. export { S3Gateway } from "@cloudflare/sandbox";然后添加存储桶的端点和名称作为变量,并将其凭据作为密钥,如 挂载 R2 存储桶 的第 3 步和第 4 步所示。0.12 从环境读取
AWS_*和R2_*变量。S3Mount只使用你传给它的凭据。1.0 无法通过 R2 绑定挂载存储桶。如果你的 0.12 代码用绑定名称调用
mountBucket()且没有endpoint,请为该存储桶创建具有 Object Read & Write 权限的 R2 API 令牌。 -
将
mountBucket()替换为为 sandbox 挂载前缀的方法:src/index.ts (0.12)ts const sandbox = getSandbox(env.Sandbox, job); await sandbox.mountBucket(env.S3_BUCKET, "/data", { endpoint: env.S3_ENDPOINT, prefix: `/jobs/${job}/`, });src/index.ts (1.0)ts export class MySandbox extends DurableObject<Env> { // ... // One S3Mount serves every container of this Durable Object. private readonly mounts = new S3Mount( this.container, this.ctx.exports.S3Gateway, ); private async mountData(): Promise<void> { // The name that your Worker passed to getByName(), // or to getSandbox() in 0.12 const job = this.ctx.id.name; if (!job) { throw new Error("Open the sandbox with getByName()"); } await this.mounts.mount({ mountPath: "/data", source: { type: "s3", endpoint: this.env.S3_ENDPOINT, region: "auto", bucket: this.env.S3_BUCKET, credentials: { type: "static", accessKeyId: this.env.S3_ACCESS_KEY_ID, secretAccessKey: this.env.S3_SECRET_ACCESS_KEY, }, }, keyPrefix: `jobs/${job}`, access: "read-write", }); } }选项变化如下:
0.12 1.0 bucket参数source.bucketendpointsource.endpoint,以及region(R2 使用auto)prefix: "/jobs/ada/"keyPrefix: "jobs/ada"。不能以/开头,S3Mount会添加尾部/readOnly: trueaccess: "read-only"。access是必填的s3fsOptions: ["name=value"]s3fsOptions: { name: "value" }。S3Mount始终设置nomixupload(0.12 仅对 R2 添加它),并在此处拒绝该选项credentialssource.credentials,带有type: "static"provider、credentialProxy移除它们。 S3Gateway为每个请求签名 -
在每个使用该前缀的方法中挂载它:
src/index.tsts export class MySandbox extends DurableObject<Env> { // ... async listFiles(): Promise<string> { await this.ensureRunning(); await this.mountData(); const ls = await this.container.exec(["ls", "-la"], { cwd: "/data", env: ENV, }); const output = await ls.output(); return new TextDecoder().decode(output.stdout); } }挂载会持续到容器停止,在 0.12 和 1.0 中都是如此。使用相同请求再次调用
mount()会复用该挂载。对已挂载路径使用其他设置的请求会抛出代码为S3_MOUNT_CONFLICT的SandboxS3MountError,而 0.12 会抛出InvalidMountConfigError。 -
替换
unmountBucket(),并通过code检查错误:src/index.ts (0.12)ts await sandbox.unmountBucket("/data");src/index.ts (1.0)ts export class MySandbox extends DurableObject<Env> { // ... async unmountData(): Promise<string> { try { await this.mounts.unmount("/data"); return "unmounted"; } catch (error) { if (SandboxS3MountError.is(error)) { return error.code; } throw error; } } }S3_MOUNT_BUSY表示仍有进程使用该路径。对该路径的请求会保持被拒绝。停止该进程后再次调用unmount()。0.12 错误的映射如下:0.12 1.0 InvalidMountConfigError无效请求(例如以 /开头的keyPrefix)为TypeError,路径已被占用时为S3_MOUNT_CONFLICTMissingCredentialsError无。 source.credentials是必填的S3FSMountError、BucketMountErrorS3_MOUNT_FAILEDBucketUnmountError有进程使用该路径时为 S3_MOUNT_BUSY
每个挂载都会为其自己的主机名注册 intercept。只有当挂载在 迁移出站规则 中的 interceptAllOutboundHttp() 调用之前运行时,该 intercept 才会接收请求。在该调用之后挂载会失败并出现 cannot verify the new FUSE connection,因为你的 Outbound entrypoint 会接收存储请求。稍后再次注册 catch-all 不会影响先出现的挂载。
在 startContainer() 的 try 块中、在 intercept() 之前挂载前缀:
export class MySandbox extends DurableObject<Env> {
// ...
private async startContainer(): Promise<void> {
const newContainer = !this.container.running;
if (newContainer) {
this.container.start({
image: this.container.images.sandbox,
instance: "standard-1",
env: ENV,
enableInternet: ENABLE_INTERNET,
});
}
try {
if (newContainer) {
// Replaces onStart().
await this.ctx.storage.put("startedAt", Date.now());
}
// Mount before the catch-all intercept.
await this.mountData();
await this.intercept();
await this.trust();
await this.container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
} catch (error) {
// The next request starts a new container.
await this.container.destroy();
throw error;
}
}
}从其他方法中移除 mountData() 调用。在 intercept() 之后挂载的路径无法工作,因此请在此处挂载 sandbox 需要的每个路径。要稍后挂载路径,请打开新的 sandbox。重启后的 Durable Object 会再次运行 startContainer(),mount() 会复用已匹配的挂载。
每个挂载使用容器 64 个 intercept 目标 中的一个,unmount() 不会释放它。迁移出站规则会使用两个。请给每个作业使用自己的 sandbox 名称,而不是在一个容器中为每个作业挂载新前缀。
0.12 在 wrangler dev 下使用 localBucket: true 挂载,方法是在容器和 R2 绑定之间复制文件。S3Mount 在 wrangler dev 下改为配合本机上兼容 S3 的服务器工作。请参阅 本地开发。
0.12 挂载会随 0.12 容器结束,对象留在存储桶中。切换后,直到 mountData() 运行前,新容器中不存在该挂载路径。0.12 在 prefix: "/jobs/ada/" 下写入的文件,会出现在 keyPrefix: "jobs/ada" 下的相同路径。
如果你的 0.12 类也有出站规则,已存储的规则可能包含名为 r2EgressMount 和 s3CredentialProxyMount 的 handler,用于切换时处于活动状态的挂载。来自迁移出站规则的 entrypoint 会跳过它们。
部署切换后,在 0.12 中挂载过其前缀的 sandbox 上调用 listFiles()。它会列出 0.12 写入的文件:
total 9
drwxrwxrwx 1 root root 4096 Jan 1 1970 .
drwxr-xr-x 1 2346 2346 140 Sep 27 20:20 ..
-rw-r--r-- 1 root root 20 Sep 27 20:11 hello.txt
drwxr-xr-x 1 root root 4096 Sep 27 20:11 subthis.mounts.inspect("/data") 会报告挂载状态。正常工作的挂载会返回 attachment 状态 managed、fuse 状态 connected,以及 gateway 状态 reachable 且 upstream 状态为 usable。