the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add backup and restore for Cloudflare sandboxes

+72 -1
+3
apps/cf-sandbox/bun.lock
··· 11 11 "@tsndr/cloudflare-worker-jwt": "^3.2.1", 12 12 "@types/ramda": "^0.31.1", 13 13 "consola": "^3.4.2", 14 + "dayjs": "^1.11.20", 14 15 "dotenv": "^17.2.4", 15 16 "drizzle-orm": "^0.45.1", 16 17 "effect": "^3.19.16", ··· 286 287 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 287 288 288 289 "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], 290 + 291 + "dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], 289 292 290 293 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 291 294
+1
apps/cf-sandbox/package.json
··· 32 32 "@tsndr/cloudflare-worker-jwt": "^3.2.1", 33 33 "@types/ramda": "^0.31.1", 34 34 "consola": "^3.4.2", 35 + "dayjs": "^1.11.20", 35 36 "dotenv": "^17.2.4", 36 37 "drizzle-orm": "^0.45.1", 37 38 "effect": "^3.19.16",
+9
apps/cf-sandbox/src/providers/cloudflare/index.ts
··· 197 197 async stopService(id: string): Promise<void> { 198 198 await this.sandbox.killProcess(id); 199 199 } 200 + 201 + async backup(dir: string, ttl?: number): Promise<string> { 202 + const { id } = await this.sandbox.createBackup({ dir, ttl }); 203 + return id; 204 + } 205 + 206 + async restore(id: string, dir: string): Promise<void> { 207 + await this.sandbox.restoreBackup({ id, dir }); 208 + } 200 209 } 201 210 202 211 class CloudflareProvider implements BaseProvider {
+2
apps/cf-sandbox/src/providers/index.ts
··· 27 27 abstract unexposeVscode(): Promise<void>; 28 28 abstract startService(command: string): Promise<string>; 29 29 abstract stopService(id: string): Promise<void>; 30 + abstract backup(dir: string, ttl?: number): Promise<string>; 31 + abstract restore(id: string, dir: string): Promise<void>; 30 32 } 31 33 32 34 abstract class BaseProvider {
+42 -1
apps/cf-sandbox/src/routes/sandboxes.ts
··· 13 13 } from "../types/sandbox"; 14 14 import { createSandbox } from "../providers"; 15 15 import { SelectSandbox } from "../schema/sandboxes"; 16 - import { sandboxes, users, services } from "../schema"; 16 + import { sandboxes, users, services, backups } from "../schema"; 17 17 import { 18 18 getSandboxRecord, 19 19 generateSandboxId, ··· 32 32 } from "../lib/sandbox-resources"; 33 33 import { PushDirectoryParams, pushSchema } from "../types/push"; 34 34 import { PullDirectoryParams, pullSchema } from "../types/pull"; 35 + import { BackupParams } from "../types/backup"; 36 + import dayjs from "dayjs"; 37 + import { RestoreParams } from "../types/restore"; 35 38 36 39 type Bindings = { Sandbox: DurableObjectNamespace<Sandbox<Env>> }; 37 40 type App = { Variables: Context; Bindings: Bindings }; ··· 530 533 sandboxRoutes.post("/v1/sandboxes/:sandboxId/backup", async (c) => { 531 534 const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 532 535 const record = result?.sandbox; 536 + 537 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 538 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 539 + 540 + const params = await c.req.json<BackupParams>(); 541 + 542 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 543 + const backupId = await sandbox.backup(params.directory, params.ttl); 544 + 545 + await c.var.db.insert(backups).values({ 546 + backupId, 547 + sandboxId: record.id, 548 + directory: params.directory, 549 + expiresAt: params.ttl ? dayjs().add(params.ttl, "second").toDate() : dayjs().add(3, "days").toDate(), 550 + }).execute(); 551 + 552 + return c.json({ backupId }); 533 553 }); 534 554 535 555 sandboxRoutes.post("/v1/sandboxes/:sandboxId/restore", async (c) => { 536 556 const result = await getSandboxRecord(c.var.db, c.req.param("sandboxId")); 537 557 const record = result?.sandbox; 558 + 559 + if (!record) return c.json({ error: "Sandbox not found" }, 404); 560 + if (record.provider !== "cloudflare") return c.json({ error: "Sandbox provider not supported" }, 400); 561 + 562 + const params = await c.req.json<RestoreParams>(); 563 + 564 + const [backup] = await c.var.db.select().from(backups) 565 + .where( 566 + and( 567 + eq(backups.id, params.backupId), 568 + eq(backups.sandboxId, record.id), 569 + ), 570 + ) 571 + .execute(); 572 + 573 + if (!backup) return c.json({ error: "Backup not found" }, 404); 574 + 575 + const sandbox = await createSandbox("cloudflare", { id: record.sandboxId! }); 576 + await sandbox.restore(backup.backupId, backup.directory); 577 + 578 + return c.json({ success: true }); 538 579 });
+8
apps/cf-sandbox/src/types/backup.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const pullSchema = z.object({ 4 + directory: z.string(), 5 + ttl: z.number().positive().optional(), 6 + }); 7 + 8 + export type BackupParams = z.infer<typeof pullSchema>;
+7
apps/cf-sandbox/src/types/restore.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const restoreSchema = z.object({ 4 + backupId: z.string(), 5 + }); 6 + 7 + export type RestoreParams = z.infer<typeof restoreSchema>;