@cloudflare/sandbox 0.8.9 → 0.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as CheckChangesOptions, A as DesktopStopResponse, At as SandboxOptions, B as ExecuteResponse, Bt as ExposePortRequest, C as ClickOptions, Ct as ProcessListResult, D as DesktopStartOptions, Dt as ProcessStatus, E as DesktopClient, Et as ProcessStartResult, F as ScreenshotRegion, Ft as WatchOptions, G as HttpClientOptions, Gt as Execution, H as BaseApiResponse, Ht as PtyOptions, I as ScreenshotResponse, It as isExecResult, J as SessionRequest, K as RequestConfig, Kt as ExecutionResult, L as ScrollDirection, Lt as isProcess, M as ScreenSizeResponse, Mt as StreamOptions, N as ScreenshotBytesResponse, Nt as WaitForLogResult, O as DesktopStartResponse, Ot as RemoteMountBucketOptions, P as ScreenshotOptions, Pt as WaitForPortOptions, Q as BucketProvider, R as TypeOptions, Rt as isProcessStatus, S as WriteFileRequest, St as ProcessKillResult, T as Desktop, Tt as ProcessOptions, U as ContainerStub, Ut as CodeContext, V as BackupClient, Vt as StartProcessRequest, W as ErrorResponse, Wt as CreateContextOptions, X as BaseExecOptions, Y as BackupOptions, Z as BucketCredentials, _ as GitClient, _t as PortExposeResult, a as CreateSessionRequest, at as ExecutionSession, b as MkdirRequest, bt as ProcessCleanupResult, c as DeleteSessionResponse, ct as FileStreamEvent, d as ProcessClient, dt as ISandbox, et as CheckChangesResult, f as PortClient, ft as ListFilesOptions, g as GitCheckoutRequest, gt as PortCloseResult, h as InterpreterClient, ht as MountBucketOptions, i as CommandsResponse, it as ExecResult, j as KeyInput, jt as SessionOptions, k as DesktopStatusResponse, kt as RestoreBackupResult, l as PingResponse, lt as FileWatchSSEEvent, m as ExecutionCallbacks, mt as LogEvent, n as getSandbox, nt as ExecEvent, o as CreateSessionResponse, ot as FileChunk, p as UnexposePortRequest, pt as LocalMountBucketOptions, q as ResponseHandler, qt as RunCodeOptions, r as SandboxClient, rt as ExecOptions, s as DeleteSessionRequest, st as FileMetadata, t as Sandbox, tt as DirectoryBackup, u as UtilityClient, ut as GitCheckoutResult, v as FileClient, vt as PortListResult, w as CursorPositionResponse, wt as ProcessLogsResult, x as ReadFileRequest, xt as ProcessInfoResult, y as FileOperationRequest, yt as Process, z as CommandClient, zt as ExecuteRequest } from "./sandbox-KLljXK8V.js";
1
+ import { $ as CheckChangesOptions, A as DesktopStopResponse, At as SandboxOptions, B as ExecuteResponse, Bt as ExposePortRequest, C as ClickOptions, Ct as ProcessListResult, D as DesktopStartOptions, Dt as ProcessStatus, E as DesktopClient, Et as ProcessStartResult, F as ScreenshotRegion, Ft as WatchOptions, G as HttpClientOptions, Gt as Execution, H as BaseApiResponse, Ht as PtyOptions, I as ScreenshotResponse, It as isExecResult, J as SessionRequest, K as RequestConfig, Kt as ExecutionResult, L as ScrollDirection, Lt as isProcess, M as ScreenSizeResponse, Mt as StreamOptions, N as ScreenshotBytesResponse, Nt as WaitForLogResult, O as DesktopStartResponse, Ot as RemoteMountBucketOptions, P as ScreenshotOptions, Pt as WaitForPortOptions, Q as BucketProvider, R as TypeOptions, Rt as isProcessStatus, S as WriteFileRequest, St as ProcessKillResult, T as Desktop, Tt as ProcessOptions, U as ContainerStub, Ut as CodeContext, V as BackupClient, Vt as StartProcessRequest, W as ErrorResponse, Wt as CreateContextOptions, X as BaseExecOptions, Y as BackupOptions, Z as BucketCredentials, _ as GitClient, _t as PortExposeResult, a as CreateSessionRequest, at as ExecutionSession, b as MkdirRequest, bt as ProcessCleanupResult, c as DeleteSessionResponse, ct as FileStreamEvent, d as ProcessClient, dt as ISandbox, et as CheckChangesResult, f as PortClient, ft as ListFilesOptions, g as GitCheckoutRequest, gt as PortCloseResult, h as InterpreterClient, ht as MountBucketOptions, i as CommandsResponse, it as ExecResult, j as KeyInput, jt as SessionOptions, k as DesktopStatusResponse, kt as RestoreBackupResult, l as PingResponse, lt as FileWatchSSEEvent, m as ExecutionCallbacks, mt as LogEvent, n as getSandbox, nt as ExecEvent, o as CreateSessionResponse, ot as FileChunk, p as UnexposePortRequest, pt as LocalMountBucketOptions, q as ResponseHandler, qt as RunCodeOptions, r as SandboxClient, rt as ExecOptions, s as DeleteSessionRequest, st as FileMetadata, t as Sandbox, tt as DirectoryBackup, u as UtilityClient, ut as GitCheckoutResult, v as FileClient, vt as PortListResult, w as CursorPositionResponse, wt as ProcessLogsResult, x as ReadFileRequest, xt as ProcessInfoResult, y as FileOperationRequest, yt as Process, z as CommandClient, zt as ExecuteRequest } from "./sandbox-CbGbomnq.js";
2
2
  import { a as DesktopCoordinateErrorContext, d as ErrorResponse$1, f as OperationType, i as BackupRestoreContext, l as ProcessExitedBeforeReadyContext, n as BackupExpiredContext, o as DesktopErrorContext, p as ErrorCode, r as BackupNotFoundContext, s as InvalidBackupConfigContext, t as BackupCreateContext, u as ProcessReadyTimeoutContext } from "./contexts-DqnVW04M.js";
3
3
  import { ContainerProxy } from "@cloudflare/containers";
4
4
 
package/dist/index.js CHANGED
@@ -3702,7 +3702,7 @@ function buildS3fsSource(bucket, prefix) {
3702
3702
  * This file is auto-updated by .github/changeset-version.ts during releases
3703
3703
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
3704
3704
  */
3705
- const SDK_VERSION = "0.8.9";
3705
+ const SDK_VERSION = "0.8.10";
3706
3706
 
3707
3707
  //#endregion
3708
3708
  //#region src/sandbox.ts
@@ -4327,7 +4327,7 @@ var Sandbox = class Sandbox extends Container {
4327
4327
  /**
4328
4328
  * Execute S3FS mount command
4329
4329
  */
4330
- async executeS3FSMount(bucket, mountPath, options, provider, passwordFilePath) {
4330
+ async executeS3FSMount(bucket, mountPath, options, provider, passwordFilePath, sessionId) {
4331
4331
  const resolvedOptions = resolveS3fsOptions(provider, options.s3fsOptions);
4332
4332
  const s3fsArgs = [];
4333
4333
  s3fsArgs.push(`passwd_file=${passwordFilePath}`);
@@ -4336,7 +4336,7 @@ var Sandbox = class Sandbox extends Container {
4336
4336
  s3fsArgs.push(`url=${options.endpoint}`);
4337
4337
  const optionsStr = shellEscape(s3fsArgs.join(","));
4338
4338
  const mountCmd = `s3fs ${shellEscape(bucket)} ${shellEscape(mountPath)} -o ${optionsStr}`;
4339
- const result = await this.execInternal(mountCmd);
4339
+ const result = sessionId ? await this.execWithSession(mountCmd, sessionId, { origin: "internal" }) : await this.execInternal(mountCmd);
4340
4340
  if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
4341
4341
  }
4342
4342
  /**
@@ -5803,74 +5803,54 @@ var Sandbox = class Sandbox extends Container {
5803
5803
  }
5804
5804
  }
5805
5805
  /**
5806
- * Download a backup archive via presigned GET URL.
5807
- * The container curls the archive directly from R2, bypassing the DO.
5808
- * ~93 MB/s throughput vs ~0.6 MB/s for base64 writeFile.
5806
+ * Mount a backup archive from R2 via s3fs so squashfuse can read it lazily.
5809
5807
  */
5810
- async downloadBackupPresigned(archivePath, r2Key, expectedSize, backupId, dir, backupSession) {
5811
- const presignedUrl = await this.generatePresignedGetUrl(r2Key);
5812
- await this.execWithSession("mkdir -p /var/backups", backupSession, { origin: "internal" });
5813
- const tmpPath = `${archivePath}.tmp`;
5814
- const curlCmd = [
5815
- "curl -sSf",
5816
- "--connect-timeout 10",
5817
- "--max-time 1800",
5818
- "--retry 2",
5819
- "--retry-max-time 60",
5820
- `-o ${shellEscape(tmpPath)}`,
5821
- shellEscape(presignedUrl)
5822
- ].join(" ");
5823
- const result = await this.execWithSession(curlCmd, backupSession, {
5824
- timeout: 181e4,
5825
- origin: "internal"
5808
+ async mountBackupR2(mountPath, prefix, backupSession) {
5809
+ const { accountId, bucketName } = this.requirePresignedUrlSupport();
5810
+ const endpoint = `https://${accountId}.r2.cloudflarestorage.com`;
5811
+ const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
5812
+ const options = {
5813
+ endpoint,
5814
+ provider: "r2",
5815
+ readOnly: true,
5816
+ prefix: normalizedPrefix,
5817
+ s3fsOptions: ["use_path_request_style"]
5818
+ };
5819
+ const passwordFilePath = this.generatePasswordFilePath();
5820
+ const s3fsSource = buildS3fsSource(bucketName, normalizedPrefix);
5821
+ const envObj = this.env;
5822
+ const credentials = detectCredentials(options, {
5823
+ AWS_ACCESS_KEY_ID: getEnvString(envObj, "AWS_ACCESS_KEY_ID"),
5824
+ AWS_SECRET_ACCESS_KEY: getEnvString(envObj, "AWS_SECRET_ACCESS_KEY"),
5825
+ R2_ACCESS_KEY_ID: this.r2AccessKeyId || void 0,
5826
+ R2_SECRET_ACCESS_KEY: this.r2SecretAccessKey || void 0,
5827
+ ...this.envVars
5826
5828
  });
5827
- if (result.exitCode !== 0) {
5828
- await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
5829
- throw new BackupRestoreError({
5830
- message: `Presigned URL download failed (exit code ${result.exitCode}): ${result.stderr}`,
5831
- code: ErrorCode.BACKUP_RESTORE_FAILED,
5832
- httpStatus: 500,
5833
- context: {
5834
- dir,
5835
- backupId
5836
- },
5837
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5838
- });
5839
- }
5840
- const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" });
5841
- const actualSize = parseInt(sizeCheck.stdout.trim(), 10);
5842
- if (actualSize !== expectedSize) {
5843
- await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
5844
- throw new BackupRestoreError({
5845
- message: `Downloaded archive size mismatch: expected ${expectedSize}, got ${actualSize}`,
5846
- code: ErrorCode.BACKUP_RESTORE_FAILED,
5847
- httpStatus: 500,
5848
- context: {
5849
- dir,
5850
- backupId
5851
- },
5852
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5853
- });
5854
- }
5855
- const mvResult = await this.execWithSession(`mv ${shellEscape(tmpPath)} ${shellEscape(archivePath)}`, backupSession, { origin: "internal" });
5856
- if (mvResult.exitCode !== 0) {
5857
- await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
5858
- throw new BackupRestoreError({
5859
- message: `Failed to finalize downloaded archive: ${mvResult.stderr}`,
5860
- code: ErrorCode.BACKUP_RESTORE_FAILED,
5861
- httpStatus: 500,
5862
- context: {
5863
- dir,
5864
- backupId
5865
- },
5866
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5867
- });
5829
+ const mountInfo = {
5830
+ mountType: "fuse",
5831
+ bucket: s3fsSource,
5832
+ mountPath,
5833
+ endpoint,
5834
+ provider: "r2",
5835
+ passwordFilePath,
5836
+ mounted: false
5837
+ };
5838
+ this.activeMounts.set(mountPath, mountInfo);
5839
+ try {
5840
+ await this.createPasswordFile(passwordFilePath, bucketName, credentials);
5841
+ await this.execWithSession(`mkdir -p ${shellEscape(mountPath)}`, backupSession, { origin: "internal" });
5842
+ await this.executeS3FSMount(s3fsSource, mountPath, options, "r2", passwordFilePath, backupSession);
5843
+ mountInfo.mounted = true;
5844
+ } catch (error) {
5845
+ await this.deletePasswordFile(passwordFilePath);
5846
+ this.activeMounts.delete(mountPath);
5847
+ throw error;
5868
5848
  }
5869
5849
  }
5870
5850
  /**
5871
5851
  * Serialize backup operations on this sandbox instance.
5872
5852
  * Concurrent backup/restore calls are queued so the multi-step
5873
- * create-archive → read → upload (or downloadwrite → extract) flow
5853
+ * create-archive → read → upload (or mount → extract) flow
5874
5854
  * is not interleaved with another backup operation on the same directory.
5875
5855
  */
5876
5856
  enqueueBackupOp(fn) {
@@ -6017,7 +5997,7 @@ var Sandbox = class Sandbox extends Container {
6017
5997
  *
6018
5998
  * Flow:
6019
5999
  * 1. DO reads metadata from R2 and checks TTL
6020
- * 2. Container downloads the archive directly from R2 via presigned URL
6000
+ * 2. Container mounts the backup archive from R2 via s3fs
6021
6001
  * 3. Container mounts the squashfs archive with FUSE overlayfs
6022
6002
  *
6023
6003
  * The target directory becomes an overlay mount with the backup as a
@@ -6101,8 +6081,7 @@ var Sandbox = class Sandbox extends Container {
6101
6081
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6102
6082
  });
6103
6083
  const r2Key = `backups/${id}/data.sqsh`;
6104
- const archiveHead = await bucket.head(r2Key);
6105
- if (!archiveHead) throw new BackupNotFoundError({
6084
+ if (!await bucket.head(r2Key)) throw new BackupNotFoundError({
6106
6085
  message: `Backup archive not found in R2: ${id}. The archive may have been deleted by R2 lifecycle rules.`,
6107
6086
  code: ErrorCode.BACKUP_NOT_FOUND,
6108
6087
  httpStatus: 404,
@@ -6110,12 +6089,19 @@ var Sandbox = class Sandbox extends Container {
6110
6089
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6111
6090
  });
6112
6091
  backupSession = await this.ensureBackupSession();
6113
- const archivePath = `/var/backups/${id}.sqsh`;
6114
- const mountGlob = `/var/backups/mounts/${id}`;
6092
+ const r2MountPath = `/var/backups/r2mount/${id}`;
6093
+ const archivePath = `${r2MountPath}/data.sqsh`;
6094
+ const mountGlob = `/var/backups/mounts/r2mount/${id}/data`;
6115
6095
  await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession, { origin: "internal" }).catch(() => {});
6116
6096
  await this.execWithSession(`for d in ${shellEscape(mountGlob)}_*/lower ${shellEscape(mountGlob)}/lower; do [ -d "$d" ] && /usr/bin/fusermount3 -uz "$d" 2>/dev/null; done; true`, backupSession, { origin: "internal" }).catch(() => {});
6117
- const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession, { origin: "internal" }).catch(() => ({ stdout: "0" }));
6118
- if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size, id, dir, backupSession);
6097
+ await this.execWithSession(`/usr/bin/fusermount3 -u ${shellEscape(r2MountPath)} 2>/dev/null; /usr/bin/fusermount3 -uz ${shellEscape(r2MountPath)} 2>/dev/null; true`, backupSession, { origin: "internal" }).catch(() => {});
6098
+ const previousBackupMount = this.activeMounts.get(r2MountPath);
6099
+ if (previousBackupMount?.mountType === "fuse") {
6100
+ previousBackupMount.mounted = false;
6101
+ this.activeMounts.delete(r2MountPath);
6102
+ await this.deletePasswordFile(previousBackupMount.passwordFilePath);
6103
+ }
6104
+ await this.mountBackupR2(r2MountPath, `backups/${id}/`, backupSession);
6119
6105
  if (!(await this.client.backup.restoreArchive(dir, archivePath, backupSession)).success) throw new BackupRestoreError({
6120
6106
  message: "Container failed to restore backup archive",
6121
6107
  code: ErrorCode.BACKUP_RESTORE_FAILED,
@@ -6134,10 +6120,6 @@ var Sandbox = class Sandbox extends Container {
6134
6120
  };
6135
6121
  } catch (error) {
6136
6122
  caughtError = error instanceof Error ? error : new Error(String(error));
6137
- if (id && backupSession) {
6138
- const archivePath = `/var/backups/${id}.sqsh`;
6139
- await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
6140
- }
6141
6123
  throw error;
6142
6124
  } finally {
6143
6125
  if (backupSession) await this.client.utils.deleteSession(backupSession).catch(() => {});