@cloudflare/sandbox 0.8.4 → 0.8.6
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 +1 -1
- package/dist/index.js +121 -59
- package/dist/index.js.map +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +1 -1
- package/dist/{sandbox-BoLbdjOe.d.ts → sandbox-DW5aQ1lD.d.ts} +6 -5
- package/dist/sandbox-DW5aQ1lD.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/sandbox-BoLbdjOe.d.ts.map +0 -1
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-
|
|
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-DW5aQ1lD.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-CeQR115r.js";
|
|
3
3
|
import { ContainerProxy } from "@cloudflare/containers";
|
|
4
4
|
|
package/dist/index.js
CHANGED
|
@@ -773,6 +773,44 @@ var BaseTransport = class {
|
|
|
773
773
|
}
|
|
774
774
|
}
|
|
775
775
|
/**
|
|
776
|
+
* Build a URL targeting the container's HTTP server.
|
|
777
|
+
*/
|
|
778
|
+
buildContainerUrl(path$1) {
|
|
779
|
+
if (this.config.stub) return `http://localhost:${this.config.port || 3e3}${path$1}`;
|
|
780
|
+
return `${this.config.baseUrl ?? `http://localhost:${this.config.port || 3e3}`}${path$1}`;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Single HTTP request to the container — no WebSocket, no 503 retry.
|
|
784
|
+
*/
|
|
785
|
+
httpFetch(path$1, options) {
|
|
786
|
+
const url = this.buildContainerUrl(path$1);
|
|
787
|
+
if (this.config.stub) return this.config.stub.containerFetch(url, options || {}, this.config.port);
|
|
788
|
+
return globalThis.fetch(url, options);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Streaming HTTP request to the container — no WebSocket, no 503 retry.
|
|
792
|
+
*/
|
|
793
|
+
async httpFetchStream(path$1, body, method = "POST", headers) {
|
|
794
|
+
const url = this.buildContainerUrl(path$1);
|
|
795
|
+
const init = {
|
|
796
|
+
method,
|
|
797
|
+
headers: body && method === "POST" ? {
|
|
798
|
+
...headers,
|
|
799
|
+
"Content-Type": "application/json"
|
|
800
|
+
} : headers,
|
|
801
|
+
body: body && method === "POST" ? JSON.stringify(body) : void 0
|
|
802
|
+
};
|
|
803
|
+
let response;
|
|
804
|
+
if (this.config.stub) response = await this.config.stub.containerFetch(url, init, this.config.port);
|
|
805
|
+
else response = await globalThis.fetch(url, init);
|
|
806
|
+
if (!response.ok) {
|
|
807
|
+
const errorBody = await response.text();
|
|
808
|
+
throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`);
|
|
809
|
+
}
|
|
810
|
+
if (!response.body) throw new Error("No response body for streaming");
|
|
811
|
+
return response.body;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
776
814
|
* Sleep utility for retry delays
|
|
777
815
|
*/
|
|
778
816
|
sleep(ms) {
|
|
@@ -787,13 +825,12 @@ var BaseTransport = class {
|
|
|
787
825
|
*
|
|
788
826
|
* Uses standard fetch API for communication with the container.
|
|
789
827
|
* HTTP is stateless, so connect/disconnect are no-ops.
|
|
828
|
+
*
|
|
829
|
+
* All HTTP request logic lives in {@link BaseTransport.httpFetch} and
|
|
830
|
+
* {@link BaseTransport.httpFetchStream}; this subclass simply wires
|
|
831
|
+
* the abstract `doFetch` / `fetchStream` hooks to those shared helpers.
|
|
790
832
|
*/
|
|
791
833
|
var HttpTransport = class extends BaseTransport {
|
|
792
|
-
baseUrl;
|
|
793
|
-
constructor(config) {
|
|
794
|
-
super(config);
|
|
795
|
-
this.baseUrl = config.baseUrl ?? "http://localhost:3000";
|
|
796
|
-
}
|
|
797
834
|
getMode() {
|
|
798
835
|
return "http";
|
|
799
836
|
}
|
|
@@ -803,36 +840,10 @@ var HttpTransport = class extends BaseTransport {
|
|
|
803
840
|
return true;
|
|
804
841
|
}
|
|
805
842
|
async doFetch(path$1, options) {
|
|
806
|
-
|
|
807
|
-
if (this.config.stub) return this.config.stub.containerFetch(url, options || {}, this.config.port);
|
|
808
|
-
return globalThis.fetch(url, options);
|
|
843
|
+
return this.httpFetch(path$1, options);
|
|
809
844
|
}
|
|
810
845
|
async fetchStream(path$1, body, method = "POST", headers) {
|
|
811
|
-
|
|
812
|
-
const options = this.buildStreamOptions(body, method, headers);
|
|
813
|
-
let response;
|
|
814
|
-
if (this.config.stub) response = await this.config.stub.containerFetch(url, options, this.config.port);
|
|
815
|
-
else response = await globalThis.fetch(url, options);
|
|
816
|
-
if (!response.ok) {
|
|
817
|
-
const errorBody = await response.text();
|
|
818
|
-
throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`);
|
|
819
|
-
}
|
|
820
|
-
if (!response.body) throw new Error("No response body for streaming");
|
|
821
|
-
return response.body;
|
|
822
|
-
}
|
|
823
|
-
buildUrl(path$1) {
|
|
824
|
-
if (this.config.stub) return `http://localhost:${this.config.port}${path$1}`;
|
|
825
|
-
return `${this.baseUrl}${path$1}`;
|
|
826
|
-
}
|
|
827
|
-
buildStreamOptions(body, method, headers) {
|
|
828
|
-
return {
|
|
829
|
-
method,
|
|
830
|
-
headers: body && method === "POST" ? {
|
|
831
|
-
...headers,
|
|
832
|
-
"Content-Type": "application/json"
|
|
833
|
-
} : headers,
|
|
834
|
-
body: body && method === "POST" ? JSON.stringify(body) : void 0
|
|
835
|
-
};
|
|
846
|
+
return this.httpFetchStream(path$1, body, method, headers);
|
|
836
847
|
}
|
|
837
848
|
};
|
|
838
849
|
|
|
@@ -899,10 +910,30 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
899
910
|
this.cleanup();
|
|
900
911
|
}
|
|
901
912
|
/**
|
|
902
|
-
*
|
|
913
|
+
* Whether a WebSocket connection is currently being established.
|
|
914
|
+
*
|
|
915
|
+
* When true, awaiting `connectPromise` from a nested call would deadlock:
|
|
916
|
+
* the outer `connectViaFetch → stub.fetch → containerFetch →
|
|
917
|
+
* startAndWaitForPorts → blockConcurrencyWhile(onStart)` chain may call
|
|
918
|
+
* back into the SDK (e.g. `exec()`), which would await the same
|
|
919
|
+
* `connectPromise` that cannot resolve until `onStart` returns.
|
|
920
|
+
*
|
|
921
|
+
* Callers use this to fall back to a direct HTTP request, which is safe
|
|
922
|
+
* because `startAndWaitForPorts()` calls `setHealthy()` before invoking
|
|
923
|
+
* `onStart()`, so `containerFetch()` routes directly to the container.
|
|
924
|
+
*/
|
|
925
|
+
isWebSocketConnecting() {
|
|
926
|
+
return this.state === "connecting";
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Transport-specific fetch implementation.
|
|
903
930
|
* Converts WebSocket response to standard Response object.
|
|
931
|
+
*
|
|
932
|
+
* Falls back to HTTP while a WebSocket connection is being established
|
|
933
|
+
* to avoid the re-entrant deadlock described in `isWebSocketConnecting()`.
|
|
904
934
|
*/
|
|
905
935
|
async doFetch(path$1, options) {
|
|
936
|
+
if (this.isWebSocketConnecting()) return this.httpFetch(path$1, options);
|
|
906
937
|
await this.connect();
|
|
907
938
|
const method = options?.method || "GET";
|
|
908
939
|
const body = this.parseBody(options?.body);
|
|
@@ -914,7 +945,9 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
914
945
|
});
|
|
915
946
|
}
|
|
916
947
|
/**
|
|
917
|
-
* Streaming fetch implementation
|
|
948
|
+
* Streaming fetch implementation.
|
|
949
|
+
*
|
|
950
|
+
* Delegates to `requestStream()`, which applies the re-entrancy guard.
|
|
918
951
|
*/
|
|
919
952
|
async fetchStream(path$1, body, method = "POST", headers) {
|
|
920
953
|
return this.requestStream(method, path$1, body, headers);
|
|
@@ -1054,7 +1087,12 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1054
1087
|
});
|
|
1055
1088
|
}
|
|
1056
1089
|
/**
|
|
1057
|
-
* Send a request and wait for response
|
|
1090
|
+
* Send a request and wait for response.
|
|
1091
|
+
*
|
|
1092
|
+
* Only reachable from `doFetch()`, which already applies the re-entrancy
|
|
1093
|
+
* guard via `isWebSocketConnecting()`. The `connect()` call here handles
|
|
1094
|
+
* the case where the WebSocket was closed between `doFetch` and `request`
|
|
1095
|
+
* (idle disconnect).
|
|
1058
1096
|
*/
|
|
1059
1097
|
async request(method, path$1, body, headers) {
|
|
1060
1098
|
await this.connect();
|
|
@@ -1105,7 +1143,7 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1105
1143
|
});
|
|
1106
1144
|
}
|
|
1107
1145
|
/**
|
|
1108
|
-
* Send a streaming request and return a ReadableStream
|
|
1146
|
+
* Send a streaming request and return a ReadableStream.
|
|
1109
1147
|
*
|
|
1110
1148
|
* The stream will receive data chunks as they arrive over the WebSocket.
|
|
1111
1149
|
* Format matches SSE for compatibility with existing streaming code.
|
|
@@ -1117,8 +1155,12 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1117
1155
|
* Uses an inactivity timeout instead of a total-duration timeout so that
|
|
1118
1156
|
* long-running streams (e.g. execStream from an agent) stay alive as long
|
|
1119
1157
|
* as data is flowing. The timer resets on every chunk or response message.
|
|
1158
|
+
*
|
|
1159
|
+
* Falls back to HTTP while a WebSocket connection is being established
|
|
1160
|
+
* to avoid the re-entrant deadlock described in `isWebSocketConnecting()`.
|
|
1120
1161
|
*/
|
|
1121
1162
|
async requestStream(method, path$1, body, headers) {
|
|
1163
|
+
if (this.isWebSocketConnecting()) return this.httpFetchStream(path$1, body, method, headers);
|
|
1122
1164
|
await this.connect();
|
|
1123
1165
|
this.clearIdleDisconnectTimer();
|
|
1124
1166
|
const id = generateRequestId();
|
|
@@ -2813,6 +2855,19 @@ var SandboxClient = class {
|
|
|
2813
2855
|
}
|
|
2814
2856
|
};
|
|
2815
2857
|
|
|
2858
|
+
//#endregion
|
|
2859
|
+
//#region ../shared/src/backup.ts
|
|
2860
|
+
/**
|
|
2861
|
+
* Absolute directory prefixes supported by backup and restore operations.
|
|
2862
|
+
*/
|
|
2863
|
+
const BACKUP_ALLOWED_PREFIXES = [
|
|
2864
|
+
"/workspace",
|
|
2865
|
+
"/home",
|
|
2866
|
+
"/tmp",
|
|
2867
|
+
"/var/tmp",
|
|
2868
|
+
"/app"
|
|
2869
|
+
];
|
|
2870
|
+
|
|
2816
2871
|
//#endregion
|
|
2817
2872
|
//#region src/security.ts
|
|
2818
2873
|
/**
|
|
@@ -3632,7 +3687,7 @@ function buildS3fsSource(bucket, prefix) {
|
|
|
3632
3687
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
3633
3688
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
3634
3689
|
*/
|
|
3635
|
-
const SDK_VERSION = "0.8.
|
|
3690
|
+
const SDK_VERSION = "0.8.6";
|
|
3636
3691
|
|
|
3637
3692
|
//#endregion
|
|
3638
3693
|
//#region src/sandbox.ts
|
|
@@ -5554,7 +5609,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5554
5609
|
static UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5555
5610
|
/**
|
|
5556
5611
|
* Validate that a directory path is safe for backup operations.
|
|
5557
|
-
* Rejects empty, relative, traversal,
|
|
5612
|
+
* Rejects empty, relative, traversal, null-byte, and unsupported-root paths.
|
|
5558
5613
|
*/
|
|
5559
5614
|
static validateBackupDir(dir, label) {
|
|
5560
5615
|
if (!dir || !dir.startsWith("/")) throw new InvalidBackupConfigError({
|
|
@@ -5578,6 +5633,13 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5578
5633
|
context: { reason: `${label} must not contain ".." path segments` },
|
|
5579
5634
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5580
5635
|
});
|
|
5636
|
+
if (!BACKUP_ALLOWED_PREFIXES.some((prefix) => dir === prefix || dir.startsWith(`${prefix}/`))) throw new InvalidBackupConfigError({
|
|
5637
|
+
message: `${label} must be inside one of the supported backup roots (${BACKUP_ALLOWED_PREFIXES.join(", ")})`,
|
|
5638
|
+
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5639
|
+
httpStatus: 400,
|
|
5640
|
+
context: { reason: `${label} must be inside one of the supported backup roots` },
|
|
5641
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5642
|
+
});
|
|
5581
5643
|
}
|
|
5582
5644
|
/**
|
|
5583
5645
|
* Returns the R2 bucket or throws if backup is not configured.
|
|
@@ -5948,19 +6010,19 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5948
6010
|
const restoreStartTime = Date.now();
|
|
5949
6011
|
const bucket = this.requireBackupBucket();
|
|
5950
6012
|
this.requirePresignedUrlSupport();
|
|
5951
|
-
const { id
|
|
6013
|
+
const { id, dir } = backup;
|
|
5952
6014
|
let outcome = "error";
|
|
5953
6015
|
let caughtError;
|
|
5954
6016
|
let backupSession;
|
|
5955
6017
|
try {
|
|
5956
|
-
if (!
|
|
6018
|
+
if (!id || typeof id !== "string") throw new InvalidBackupConfigError({
|
|
5957
6019
|
message: "Invalid backup: missing or invalid id",
|
|
5958
6020
|
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5959
6021
|
httpStatus: 400,
|
|
5960
6022
|
context: { reason: "missing or invalid id" },
|
|
5961
6023
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5962
6024
|
});
|
|
5963
|
-
if (!Sandbox.UUID_REGEX.test(
|
|
6025
|
+
if (!Sandbox.UUID_REGEX.test(id)) throw new InvalidBackupConfigError({
|
|
5964
6026
|
message: "Invalid backup: id must be a valid UUID (e.g. from createBackup)",
|
|
5965
6027
|
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5966
6028
|
httpStatus: 400,
|
|
@@ -5968,13 +6030,13 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5968
6030
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5969
6031
|
});
|
|
5970
6032
|
Sandbox.validateBackupDir(dir, "Invalid backup: dir");
|
|
5971
|
-
const metaKey = `backups/${
|
|
6033
|
+
const metaKey = `backups/${id}/meta.json`;
|
|
5972
6034
|
const metaObject = await bucket.get(metaKey);
|
|
5973
6035
|
if (!metaObject) throw new BackupNotFoundError({
|
|
5974
|
-
message: `Backup not found: ${
|
|
6036
|
+
message: `Backup not found: ${id}. Verify the backup ID is correct and the backup has not been deleted.`,
|
|
5975
6037
|
code: ErrorCode.BACKUP_NOT_FOUND,
|
|
5976
6038
|
httpStatus: 404,
|
|
5977
|
-
context: { backupId },
|
|
6039
|
+
context: { backupId: id },
|
|
5978
6040
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5979
6041
|
});
|
|
5980
6042
|
const metadata = await metaObject.json();
|
|
@@ -5986,44 +6048,44 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5986
6048
|
httpStatus: 500,
|
|
5987
6049
|
context: {
|
|
5988
6050
|
dir,
|
|
5989
|
-
backupId
|
|
6051
|
+
backupId: id
|
|
5990
6052
|
},
|
|
5991
6053
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5992
6054
|
});
|
|
5993
6055
|
const expiresAt = createdAt + metadata.ttl * 1e3;
|
|
5994
6056
|
if (Date.now() + TTL_BUFFER_MS > expiresAt) throw new BackupExpiredError({
|
|
5995
|
-
message: `Backup ${
|
|
6057
|
+
message: `Backup ${id} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
|
|
5996
6058
|
code: ErrorCode.BACKUP_EXPIRED,
|
|
5997
6059
|
httpStatus: 400,
|
|
5998
6060
|
context: {
|
|
5999
|
-
backupId,
|
|
6061
|
+
backupId: id,
|
|
6000
6062
|
expiredAt: new Date(expiresAt).toISOString()
|
|
6001
6063
|
},
|
|
6002
6064
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6003
6065
|
});
|
|
6004
|
-
const r2Key = `backups/${
|
|
6066
|
+
const r2Key = `backups/${id}/data.sqsh`;
|
|
6005
6067
|
const archiveHead = await bucket.head(r2Key);
|
|
6006
6068
|
if (!archiveHead) throw new BackupNotFoundError({
|
|
6007
|
-
message: `Backup archive not found in R2: ${
|
|
6069
|
+
message: `Backup archive not found in R2: ${id}. The archive may have been deleted by R2 lifecycle rules.`,
|
|
6008
6070
|
code: ErrorCode.BACKUP_NOT_FOUND,
|
|
6009
6071
|
httpStatus: 404,
|
|
6010
|
-
context: { backupId },
|
|
6072
|
+
context: { backupId: id },
|
|
6011
6073
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6012
6074
|
});
|
|
6013
6075
|
backupSession = await this.ensureBackupSession();
|
|
6014
|
-
const archivePath = `/var/backups/${
|
|
6015
|
-
const mountGlob = `/var/backups/mounts/${
|
|
6076
|
+
const archivePath = `/var/backups/${id}.sqsh`;
|
|
6077
|
+
const mountGlob = `/var/backups/mounts/${id}`;
|
|
6016
6078
|
await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession, { origin: "internal" }).catch(() => {});
|
|
6017
6079
|
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(() => {});
|
|
6018
6080
|
const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession, { origin: "internal" }).catch(() => ({ stdout: "0" }));
|
|
6019
|
-
if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size,
|
|
6081
|
+
if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size, id, dir, backupSession);
|
|
6020
6082
|
if (!(await this.client.backup.restoreArchive(dir, archivePath, backupSession)).success) throw new BackupRestoreError({
|
|
6021
6083
|
message: "Container failed to restore backup archive",
|
|
6022
6084
|
code: ErrorCode.BACKUP_RESTORE_FAILED,
|
|
6023
6085
|
httpStatus: 500,
|
|
6024
6086
|
context: {
|
|
6025
6087
|
dir,
|
|
6026
|
-
backupId
|
|
6088
|
+
backupId: id
|
|
6027
6089
|
},
|
|
6028
6090
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6029
6091
|
});
|
|
@@ -6031,12 +6093,12 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6031
6093
|
return {
|
|
6032
6094
|
success: true,
|
|
6033
6095
|
dir,
|
|
6034
|
-
id
|
|
6096
|
+
id
|
|
6035
6097
|
};
|
|
6036
6098
|
} catch (error) {
|
|
6037
6099
|
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
6038
|
-
if (
|
|
6039
|
-
const archivePath = `/var/backups/${
|
|
6100
|
+
if (id && backupSession) {
|
|
6101
|
+
const archivePath = `/var/backups/${id}.sqsh`;
|
|
6040
6102
|
await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
6041
6103
|
}
|
|
6042
6104
|
throw error;
|
|
@@ -6046,7 +6108,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6046
6108
|
event: "backup.restore",
|
|
6047
6109
|
outcome,
|
|
6048
6110
|
durationMs: Date.now() - restoreStartTime,
|
|
6049
|
-
backupId,
|
|
6111
|
+
backupId: id,
|
|
6050
6112
|
dir,
|
|
6051
6113
|
error: caughtError
|
|
6052
6114
|
});
|