DirectoryBackup 会将正在运行的 container 中的目录保存到 R2 bucket,并稍后将其恢复到 container 中,包括运行较新镜像的 container。container 通过 Worker 中的 DirectoryBackupGateway 发送备份,因此 container 不需要互联网访问和 R2 凭据。gateway 只让 container 访问操作所需的对象。DirectoryBackup 不会启动或停止 container。
import { DirectoryBackup } from "@cloudflare/sandbox";
import { DurableObject } from "cloudflare:workers";
export { DirectoryBackupGateway } from "@cloudflare/sandbox";
export class MyContainer extends DurableObject {
#backups;
constructor(ctx, env) {
super(ctx, env);
if (!ctx.container) {
throw new Error("No container is configured");
}
// ctx.container stays the same object while the Durable Object runs.
this.#backups = new DirectoryBackup(
ctx.container,
ctx.exports.DirectoryBackupGateway,
{ binding: "BACKUPS", prefix: "workspaces/" },
);
}
// The container must already be running.
async saveWorkspace() {
const backup = await this.#backups.backup({
dir: "/workspace/app",
exclude: ["node_modules/"],
});
// Keep the record. The package does not list backups.
await this.ctx.storage.put("workspace", backup);
}
async restoreWorkspace() {
const backup = await this.ctx.storage.get("workspace");
if (backup) {
await this.#backups.restore(backup);
}
}
}import {
DirectoryBackup,
type DirectoryBackupRecord,
} from "@cloudflare/sandbox";
import { DurableObject } from "cloudflare:workers";
export { DirectoryBackupGateway } from "@cloudflare/sandbox";
export class MyContainer extends DurableObject<Env> {
readonly #backups: DirectoryBackup;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
if (!ctx.container) {
throw new Error("No container is configured");
}
// ctx.container stays the same object while the Durable Object runs.
this.#backups = new DirectoryBackup(
ctx.container,
ctx.exports.DirectoryBackupGateway,
{ binding: "BACKUPS", prefix: "workspaces/" },
);
}
// The container must already be running.
async saveWorkspace() {
const backup = await this.#backups.backup({
dir: "/workspace/app",
exclude: ["node_modules/"],
});
// Keep the record. The package does not list backups.
await this.ctx.storage.put("workspace", backup);
}
async restoreWorkspace() {
const backup =
await this.ctx.storage.get<DirectoryBackupRecord>("workspace");
if (backup) {
await this.#backups.restore(backup);
}
}
}完整 Worker 请参阅 将目录备份到 R2。
除了软件包要求外,DirectoryBackup 还有这些要求:
-
Worker 具有 R2 bucket 绑定。
-
Worker 的主模块从
@cloudflare/sandbox重新导出DirectoryBackupGateway:export { DirectoryBackupGateway } from "@cloudflare/sandbox";不要将 HTTP 请求路由到
DirectoryBackupGateway。
new DirectoryBackup(
container: Pick<Container, "exec" | "interceptOutboundHttp">,
gateway: DirectoryBackupGatewayBinding,
storage: DirectoryBackupStorage,
)container— 来自this.ctx.container的 container。gateway—this.ctx.exports.DirectoryBackupGateway.storage— 备份的存储位置。请参阅DirectoryBackupStorage。
无效的 storage 会抛出 TypeError。
backup(options: DirectoryBackupOptions): Promise<DirectoryBackupRecord>将 options.dir 保存为 bucket 中的一个对象,并返回 DirectoryBackupRecord。选项请参阅 DirectoryBackupOptions。备份包含什么 列出对象会保留的内容。
backup() 会在进程仍可更改目录时读取它,并且不会检测更改。备份期间更改的文件可能一部分是旧的、一部分是新的。只有在运行期间没有进程写入 dir 时,备份才是一致的。
失败或被取消的备份不会返回记录,backup() 会中止其 multipart upload。如果 Durable Object 在备份期间重启,上传会留在 bucket 中,直到 R2 生命周期规则 中止它。
restore(backup: DirectoryBackupRecord, options?: DirectoryRestoreOptions): Promise<void>用备份的内容替换目标目录。目标是记录中的 dir,除非你在 options.dir 中传入另一个绝对路径。
- 目标的父目录必须存在。目标本身不必存在。
- 目标不得是挂载点,也不得包含挂载点,例如
S3Mount挂载。否则restore()会以EBUSY失败,包括恢复期间出现挂载的情况。请先卸载(例如使用S3Mount.unmount()),并在恢复后再挂载。 restore()会将备份提取到目标旁边的新目录,并对照记录检查对象的大小和 SHA-256。然后它用一次原子重命名将目标替换为新目录。当restore()抛出SandboxFileError或SandboxBackupError,或在验证下载之前被取消时,它会保持目标原样并移除新目录。- 工作目录位于目标内,或打开了目标中文件的进程,会继续使用旧目录或文件。
restore()之后请重启这些进程,或让它们按绝对路径重新打开文件。 - 文件系统需要同时有旧目录和恢复目录的空闲空间。在空闲空间降到文件系统的 5% 或 256 MiB(取较小者)之前,
restore()会以ENOSPC停止。 - 已部署 container 的根文件系统会压缩文件。
restore()写入的文件不压缩。在进程重写它们之前,恢复的文件会占用更多磁盘空间。
delete(backup: DirectoryBackupRecord, options?: DirectoryBackupDeleteOptions): Promise<void>删除备份对象。container 不必正在运行。删除不存在的对象会成功。同时读取该对象的 restore() 会以 BACKUP_NOT_FOUND 或 BACKUP_INTEGRITY 失败。
intercept(): Promise<void>将 container 的备份流量路由到 DirectoryBackupGateway,后者在备份或恢复开始前会拒绝每个请求。intercept() 不会启动 container。
backup() 和 restore() 会自行添加此路由。如果 Durable Object 还调用 interceptAllOutboundHttp(),请在它之前调用 intercept()。否则 catch-all 会收到来自 container 的备份流量,backup() 和 restore() 会以 BACKUP_TRANSFER 失败。
- 在启动 container 后运行的设置中调用
intercept(),并在重启后的 Durable Object 发现 container 仍在运行时再次调用。然后该 container 中的备份和恢复会保持其路由。 - 不要在备份或恢复可能正在运行时调用
intercept()。它会替换正在运行操作的访问权限,导致操作失败。 - 先注册了 catch-all 的 container 会继续将备份流量发送给它。请重启 container 以使用
intercept()。
#setup: Promise<void> | null = null;
// Call before each use of the container.
#ensureRunning(container: Container): Promise<void> {
// Set up each new container, and a running container
// after this Durable Object restarts.
if (this.#setup === null || !container.running) {
this.#setup = this.#setUp(container).catch((error) => {
this.#setup = null;
throw error;
});
}
return this.#setup;
}
async #setUp(container: Container): Promise<void> {
if (!container.running) {
container.start({
image: container.images.sandbox,
enableInternet: false,
});
}
await this.#backups.intercept();
await container.interceptAllOutboundHttp(
this.ctx.exports.Outbound({ props: {} }),
);
}backup() 返回的记录。它是可以存储在 Durable Object 存储、数据库或 JSON 中的普通对象。该软件包不维护备份列表。
idstring— 备份的 UUID。对象键为<prefix><id>.tar.zst。dirstring— 已备份目录的绝对路径,也是默认恢复目标。sizenumber— 存储对象的大小(字节)。namestringoptional — 传给backup()的name。sha256string— 存储对象的 SHA-256,为 64 个小写十六进制字符。每次restore()都会检查它。format"tar+zstd/1"— 对象的归档格式。
restore() 和 delete() 会使用创建 DirectoryBackup 实例时指定的存储桶和前缀中的 <prefix><id>.tar.zst 对象,因此应将记录传给存有该对象的实例。restore() 会将对象的大小和 SHA-256 与记录进行比对。与此格式不符的记录会抛出 TypeError。
bindingstring— Workerenv中 R2 bucket 绑定的名称,例如"BACKUPS"。prefixstringoptional — 对象的键前缀。非空前缀必须以/结尾。默认没有前缀。
dirstring— 要备份的目录的绝对路径。namestringoptional — 存储在记录和对象自定义元数据中的标签,字段为name。excludestring[]optional —.gitignore语法 ↗ 的模式,相对于dir。"node_modules/"会排除每个名为node_modules的目录。"/build"只排除dir顶层的build。gitignorebooleanoptional — 同时应用dir内的.gitignore文件和.git/info/exclude。全局 Git excludes 不适用。镜像不需要git。默认为false。signalAbortSignaloptional — 取消操作。请参阅 取消。
dirstringoptional — 要替换的目录的绝对路径。默认是记录中的dir。signalAbortSignaloptional — 取消操作。请参阅 取消。
signalAbortSignaloptional — 已中止的信号会在删除对象之前拒绝调用。
方法不接受的选项,或类型错误的选项,会抛出 TypeError。
backup() 会遍历 dir 且不跟随符号链接,也不会进入其他文件系统。dir 内的挂载点在备份中会变成空目录。restore() 则相反:它会拒绝包含挂载点的目标。更多信息请参阅 restore。除非 exclude 或 gitignore 将其排除,否则 backup() 会包含所有其他路径,包括 .git。
备份会保留:
- 文件内容
- 目录,包括空目录
- 符号链接(作为链接)
dir内文件之间的硬链接- 权限位,以及数字用户和组 ID
- 修改时间,精确到纳秒
备份不会保留:
- Socket、设备和 FIFO
- setuid 和 setgid 位
- 扩展属性、ACL 和文件 capabilities
- 稀疏文件中的空洞,恢复后为全零
- 访问时间
当镜像用户是 root 时,restore() 会将保存的用户和组 ID 设为数字,即使镜像中没有该 ID 的用户。否则,恢复的文件属于镜像用户。
一个 container 中一次只运行一个 backup() 或 restore()。其他调用会等待它完成,包括来自其他 DirectoryBackup 实例以及重启后 Durable Object 的调用。对多个目录使用 Promise.all() 会一个接一个运行。delete() 不会等待。
DirectoryBackup 不设置超时,也不会重试。中止 signal 会以中止原因拒绝调用,且不等待 helper 进程,包括仍在等待另一操作的调用。然后 container 中的 helper 进程会移除该操作创建的内容并退出。
restore() 验证下载后,即使 signal 中止也会完成替换目标。然后调用会在目标被替换时解析,或以替换时的错误拒绝。
Durable Object 在操作期间重启时,调用结果会丢失,helper 进程会退出。备份不会被记录。恢复要么已替换整个目标,要么保持原样。再次恢复同一备份无论哪种情况都会得到相同结果。
DirectoryBackupGateway 是在 container 和 R2 bucket 之间移动备份的 WorkerEntrypoint。
- container 只能访问正在进行的操作的对象。备份期间,它会上传该对象的分片。恢复期间,它会读取其字节范围。它无法列出或删除对象,也无法访问其他键。
- SHA-256 检查会检测备份后更改过的对象。
container 会向 backups.sandbox.internal 的 gateway 发送 HTTP 请求,DirectoryBackup 使用 interceptOutboundHttp() 路由它们。Durable Object 通过 RPC 调用 gateway 来创建、完成和中止上传,以及删除对象。container 中的第一次操作或 intercept() 会添加拦截目标。之后的每次操作会替换该目标上的 handler,而不会添加目标。
container 会以 16 MiB 的分片上传压缩备份。R2 multipart upload 最多持有 10,000 个分片,因此压缩备份最大为 160,000 MiB,约 156 GiB。更大的备份会以 BACKUP_TRANSFER 失败。
在 wrangler dev 下,container 在 Docker 中运行,restore() 无法替换属于镜像的目录。恢复会以 SandboxFileError 代码 EXDEV 失败,并保持目录原样。restore() 可以替换 container 启动后创建的目录、卷中的目录,或不存在的目标。
备份对象缺失、已更改,或无法传输。
| 字段 | 类型 | 说明 |
|---|---|---|
name |
"SandboxBackupError" |
错误名称 |
code |
SandboxBackupErrorCode |
错误代码 |
operation |
DirectoryBackupOperation |
失败的方法:backup 或 restore |
path |
string |
正在备份或恢复的目录 |
detail |
string |
错误描述 |
code 是以下值之一:
| 代码 | 含义 |
|---|---|
BACKUP_NOT_FOUND |
对象不存在 |
BACKUP_INTEGRITY |
对象与记录不匹配,或不是有效归档。目标未被更改。当 R2 存储的大小与 container 上传的不同时,backup() 也会抛出它,并删除该对象 |
BACKUP_TRANSFER |
container 与 DirectoryBackupGateway 之间的上传或下载失败,包括 storage.binding 未命名 R2 bucket 绑定的情况。detail 包含 HTTP 状态。未返回记录,也未更改目录 |
该软件包导出 SandboxBackupErrorCode 和 DirectoryBackupOperation 类型,它们列出这些代码和方法。
SandboxBackupError 不是类。请使用 SandboxBackupError.is(error) 而不是 instanceof。它可以识别同一 Worker 中抛出的错误以及通过 Durable Object RPC 返回的错误。
Linux 拒绝了 container 中的操作。operation 为 backup 或 restore。请参阅 SandboxFileError。
| 代码 | 条件 |
|---|---|
EBUSY |
恢复目标是挂载点或内部有挂载点 |
EINVAL |
exclude 模式无效 |
ENOENT |
备份中的 dir 或恢复目标的父目录不存在 |
ENOSPC |
恢复会填满文件系统 |
ENOTDIR |
备份中的 dir 或恢复目标是符号链接或不是目录 |
EXDEV |
Linux 无法将恢复的目录换入位置,例如 wrangler dev 下镜像中的目录 |
其他代码(例如 EACCES)来自备份期间读取文件。
DirectoryBackup 无法完成与 helper 二进制文件的交换。请参阅 SandboxProtocolError。
DirectoryBackup 不会包装来自运行时、R2 或其输入的错误。
| 条件 | 错误 |
|---|---|
无效选项、记录或 storage |
TypeError |
storage.binding 在 backup() 或 delete() 中未命名 R2 bucket 绑定 |
来自 DirectoryBackupGateway 的 TypeError |
| container 未运行 | 来自 exec() 的 Error |
signal 已中止 |
中止原因 |
| R2 拒绝创建、完成或中止上传,或删除对象 | 来自 R2 绑定的错误 |
| 无法添加拦截 | 来自 interceptOutboundHttp() 的 Error |