本指南将上传的文件流式传入 sandbox,用 gzip 压缩,然后将压缩后的文件流式返回给调用方。
@cloudflare/sandbox 中的 Files 类会在正在运行的 Linux 实例中读写文件,exec() 运行命令。Files 不会启动实例,因此 Durable Object 会在每次文件操作前启动它。
- 来自 运行 Linux 命令 的 Worker 项目。该项目使用 Durable Object,并通过 Durable Object 调度策略 启动 container。
当您运行 wrangler deploy 时,必须在本地运行 Docker。对于大多数人来说,安装 Docker 的最佳方式是按照安装 Docker Desktop 的文档 ↗进行操作。其他工具(如 Colima ↗)也可能有效。
您可以通过在终端中运行 docker info 命令来检查 Docker 是否正常运行。如果 Docker 正在运行,该命令将成功执行。如果 Docker 未运行,docker info 命令将挂起或返回包含 "Cannot connect to the Docker daemon" 消息的错误。
-
安装软件包:
npm i @cloudflare/sandboxyarn add @cloudflare/sandboxpnpm add @cloudflare/sandboxbun add @cloudflare/sandbox -
要为
Files提供其 helper 二进制文件,请在项目根目录创建Dockerfile。如果已有 Dockerfile,请向其中添加COPY行:Dockerfiledockerfile FROM node:24-trixie-slim COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim RUN mkdir /workspace CMD ["sleep", "infinity"]cloudflare/sandbox标签必须与已安装的软件包版本匹配。不匹配会导致SandboxProtocolError。将FROM行替换为命令所需的基础镜像。 -
要构建镜像,请向
wrangler.jsonc添加nodejs_compat标志,并替换containers条目:{ "compatibility_flags": ["nodejs_compat"], "containers": [ { "class_name": "MyContainer", "scheduling_policy": "durable_object", "images": { "workspace": { "dockerfile": "./Dockerfile", }, }, }, ], }compatibility_flags = [ "nodejs_compat" ] [[containers]] class_name = "MyContainer" scheduling_policy = "durable_object" [containers.images.workspace] dockerfile = "./Dockerfile"workspace键为镜像命名。你的代码会将其作为container.images.workspace启动。 -
要在每次文件操作前启动实例,请将
src/index.ts替换为带有startSandbox()方法的MyContainer类:src/index.jsjs import { Files } from "@cloudflare/sandbox"; import { DurableObject } from "cloudflare:workers"; const workspace = "/workspace"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; export class MyContainer extends DurableObject { container; files; constructor(ctx, env) { super(ctx, env); const container = ctx.container; if (!container) { throw new Error("No container is configured"); } this.container = container; this.files = new Files(container); // A restarted Durable Object sets the timeout again. if (container.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); } } // Files does not start an instance, so start one before each operation. async startSandbox() { if (!this.container.running) { this.container.start({ image: this.container.images.workspace, enableInternet: false, }); await this.container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); } } }src/index.tsts import { Files } from "@cloudflare/sandbox"; import { DurableObject } from "cloudflare:workers"; const workspace = "/workspace"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; export class MyContainer extends DurableObject<Env> { private readonly container: Container; private readonly files: Files; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = ctx.container; if (!container) { throw new Error("No container is configured"); } this.container = container; this.files = new Files(container); // A restarted Durable Object sets the timeout again. if (container.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS), ); } } // Files does not start an instance, so start one before each operation. private async startSandbox(): Promise<void> { if (!this.container.running) { this.container.start({ image: this.container.images.workspace, enableInternet: false, }); await this.container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); } } }构造函数会为 Durable Object 创建一个
Files对象,因为只要 Durable Object 在运行,this.ctx.container就会保持为同一个对象。startSandbox()会在没有实例运行时启动workspace镜像,并在start()之后设置不活动超时。更多信息请参阅 Sandbox 生命周期。 -
要写入上传、运行命令并返回输出文件,请向
MyContainer添加compress()方法:src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async compress(upload: ReadableStream<Uint8Array>): Promise<Response> { await this.startSandbox(); const name = crypto.randomUUID(); await this.files.writeFile(name, upload, { cwd: workspace }); const process = await this.container.exec(["gzip", "--force", name], { cwd: workspace, }); const output = await process.output(); if (output.exitCode !== 0) { throw new Error(new TextDecoder().decode(output.stderr)); } return this.files.readFile(`${name}.gz`, { cwd: workspace }); } }writeFile()会将上传流式写入文件,readFile()会返回将输出文件流式传回的Response。这两个方法都不会在 Durable Object 的内存中保存整个文件。output()只保存gzip打印的内容,该输出计入 内存限制。Linux 失败(例如磁盘已满)会抛出
SandboxFileError。其code保存 Linux 错误名称,例如ENOSPC。要处理其他错误,请参阅 Files 错误。 -
要将上传发送到
compress(),请向src/index.ts添加默认导出:src/index.jsjs export default { async fetch(request, env) { const url = new URL(request.url); const match = /^\/sandboxes\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\/compress$/.exec( url.pathname, ); if (!match || request.method !== "POST" || !request.body) { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName(match[1]); return sandbox.compress(request.body); }, };src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const match = /^\/sandboxes\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\/compress$/.exec(url.pathname); if (!match || request.method !== "POST" || !request.body) { return new Response("Not found", { status: 404 }); } const sandbox = env.MY_CONTAINER.getByName(match[1]); return sandbox.compress(request.body); }, } satisfies ExportedHandler<Env>; -
部署你的 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
要测试 Worker,请将文件上传到名为
ada的 sandbox 并保存响应。将 hostname 替换为 Wrangler 打印的workers.devURL:printf 'Ada Lovelace\nGrace Hopper\n' | curl \ https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/sandboxes/ada/compress \ --data-binary @- \ --fail-with-body \ --output names.txt.gz -
解压响应:
gunzip --stdout names.txt.gzAda Lovelace Grace Hopper
Files 还有其他文件操作方法。每个方法都接受与 writeFile() 和 readFile() 相同的 cwd 选项:
| 方法 | 用途 |
|---|---|
readDirectory() |
列出目录条目 |
stat() |
读取路径的类型、大小、所有者和时间戳 |
mkdir() |
创建目录 |
rename() |
重命名或移动文件或目录 |
remove() |
删除文件,或使用 recursive: true 删除目录 |
压缩文件会留在 /workspace 中,直到实例停止。要更早删除文件,请在调用方读取响应后调用 remove()。
更多信息请参阅 Files API。
有些应用允许调用方选择路径,例如文件浏览器。Files 不会限制它打开哪些路径。要将请求限制在一个目录内,请在将每个路径传给 Files 之前检查它。
本例添加 download() 方法,只接受不含 .. 的相对路径,并对缺失文件返回 404:
import { SandboxFileError } from "@cloudflare/sandbox";
import { DurableObject } from "cloudflare:workers";
const workspace = "/workspace";
// Accept only relative paths that stay inside cwd.
function workspacePath(path) {
if (
path === "" ||
path.startsWith("/") ||
path.includes("\0") ||
path.split("/").includes("..")
) {
return null;
}
return path;
}
export class MyContainer extends DurableObject {
// ...
async download(path) {
const relativePath = workspacePath(path);
if (!relativePath) {
return new Response("Invalid path", { status: 400 });
}
await this.startSandbox();
try {
return await this.files.readFile(relativePath, { cwd: workspace });
} catch (error) {
if (SandboxFileError.is(error) && error.code === "ENOENT") {
return new Response("Not found", { status: 404 });
}
throw error;
}
}
}import { SandboxFileError } from "@cloudflare/sandbox";
import { DurableObject } from "cloudflare:workers";
const workspace = "/workspace";
// Accept only relative paths that stay inside cwd.
function workspacePath(path: string): string | null {
if (
path === "" ||
path.startsWith("/") ||
path.includes("\0") ||
path.split("/").includes("..")
) {
return null;
}
return path;
}
export class MyContainer extends DurableObject<Env> {
// ...
async download(path: string): Promise<Response> {
const relativePath = workspacePath(path);
if (!relativePath) {
return new Response("Invalid path", { status: 400 });
}
await this.startSandbox();
try {
return await this.files.readFile(relativePath, { cwd: workspace });
} catch (error) {
if (SandboxFileError.is(error) && error.code === "ENOENT") {
return new Response("Not found", { status: 404 });
}
throw error;
}
}
}- Files API:每个方法,包括
readDirectory()、rename()和remove()。 - 使用 snapshot 保存并恢复 sandbox:在实例停止后保留文件。
- 挂载 R2 bucket:写入其他系统会读取的文件。