@panorama-ai/gateway 2.30.207 → 2.30.279
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/README.md +10 -0
- package/dist/cli-providers/types.d.ts +1 -1
- package/dist/cli-providers/types.d.ts.map +1 -1
- package/dist/database.types.d.ts +1723 -172
- package/dist/database.types.d.ts.map +1 -1
- package/dist/database.types.js.map +1 -1
- package/dist/finalize-subagent-run.d.ts +2 -1
- package/dist/finalize-subagent-run.d.ts.map +1 -1
- package/dist/finalize-subagent-run.js.map +1 -1
- package/dist/index.js +3384 -241
- package/dist/index.js.map +4 -4
- package/dist/managed-runtime/config.d.ts +13 -0
- package/dist/managed-runtime/config.d.ts.map +1 -1
- package/dist/managed-runtime/config.js +31 -0
- package/dist/managed-runtime/config.js.map +1 -1
- package/dist/managed-runtime/dependencies.d.ts +2 -0
- package/dist/managed-runtime/dependencies.d.ts.map +1 -1
- package/dist/managed-runtime/dependencies.js +2 -0
- package/dist/managed-runtime/dependencies.js.map +1 -1
- package/dist/managed-runtime/drive-sync-filesystem.d.ts +39 -0
- package/dist/managed-runtime/drive-sync-filesystem.d.ts.map +1 -0
- package/dist/managed-runtime/drive-sync-filesystem.js +434 -0
- package/dist/managed-runtime/drive-sync-filesystem.js.map +1 -0
- package/dist/managed-runtime/drive-sync-planner.d.ts +76 -0
- package/dist/managed-runtime/drive-sync-planner.d.ts.map +1 -0
- package/dist/managed-runtime/drive-sync-planner.js +363 -0
- package/dist/managed-runtime/drive-sync-planner.js.map +1 -0
- package/dist/managed-runtime/drive-sync-remote-planner.d.ts +52 -0
- package/dist/managed-runtime/drive-sync-remote-planner.d.ts.map +1 -0
- package/dist/managed-runtime/drive-sync-remote-planner.js +77 -0
- package/dist/managed-runtime/drive-sync-remote-planner.js.map +1 -0
- package/dist/managed-runtime/drive-sync-scheduler.d.ts +50 -0
- package/dist/managed-runtime/drive-sync-scheduler.d.ts.map +1 -0
- package/dist/managed-runtime/drive-sync-scheduler.js +302 -0
- package/dist/managed-runtime/drive-sync-scheduler.js.map +1 -0
- package/dist/managed-runtime/drive-sync-state-applier.d.ts +84 -0
- package/dist/managed-runtime/drive-sync-state-applier.d.ts.map +1 -0
- package/dist/managed-runtime/drive-sync-state-applier.js +153 -0
- package/dist/managed-runtime/drive-sync-state-applier.js.map +1 -0
- package/dist/managed-runtime/drive-sync-transfer.d.ts +86 -0
- package/dist/managed-runtime/drive-sync-transfer.d.ts.map +1 -0
- package/dist/managed-runtime/drive-sync-transfer.js +245 -0
- package/dist/managed-runtime/drive-sync-transfer.js.map +1 -0
- package/dist/managed-runtime/drive-sync.d.ts +416 -0
- package/dist/managed-runtime/drive-sync.d.ts.map +1 -0
- package/dist/managed-runtime/drive-sync.js +1641 -0
- package/dist/managed-runtime/drive-sync.js.map +1 -0
- package/dist/managed-runtime.d.ts.map +1 -1
- package/dist/managed-runtime.js +44 -0
- package/dist/managed-runtime.js.map +1 -1
- package/dist/subagent-adapters/types.d.ts +1 -1
- package/dist/subagent-adapters/types.d.ts.map +1 -1
- package/dist/subagent-output-persistence.d.ts +1 -1
- package/dist/subagent-output-persistence.d.ts.map +1 -1
- package/dist/subagent-output-persistence.js +1 -1
- package/dist/subagent-output-persistence.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -315,12 +315,12 @@ function isGatewayHealthCheckType(value) {
|
|
|
315
315
|
// dist/index.js
|
|
316
316
|
import dotenv from "dotenv";
|
|
317
317
|
import { execFile as execFile6 } from "node:child_process";
|
|
318
|
-
import { randomUUID as
|
|
318
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
319
319
|
import fsSync9 from "node:fs";
|
|
320
320
|
import fs6 from "node:fs/promises";
|
|
321
321
|
import os8 from "node:os";
|
|
322
|
-
import
|
|
323
|
-
import
|
|
322
|
+
import path20 from "node:path";
|
|
323
|
+
import process19 from "node:process";
|
|
324
324
|
import { promisify as promisify6 } from "node:util";
|
|
325
325
|
|
|
326
326
|
// dist/cli-args.js
|
|
@@ -712,8 +712,8 @@ async function showGatewayLogsCommand(options, dependencies) {
|
|
|
712
712
|
return;
|
|
713
713
|
let position = 0;
|
|
714
714
|
try {
|
|
715
|
-
const
|
|
716
|
-
position =
|
|
715
|
+
const stat2 = await fs.stat(logPath);
|
|
716
|
+
position = stat2.size;
|
|
717
717
|
} catch {
|
|
718
718
|
position = 0;
|
|
719
719
|
}
|
|
@@ -721,16 +721,16 @@ async function showGatewayLogsCommand(options, dependencies) {
|
|
|
721
721
|
if (event !== "change")
|
|
722
722
|
return;
|
|
723
723
|
try {
|
|
724
|
-
const
|
|
725
|
-
if (
|
|
724
|
+
const stat2 = await fs.stat(logPath);
|
|
725
|
+
if (stat2.size < position) {
|
|
726
726
|
position = 0;
|
|
727
727
|
}
|
|
728
728
|
const handle = await fs.open(logPath, "r");
|
|
729
|
-
const length =
|
|
729
|
+
const length = stat2.size - position;
|
|
730
730
|
if (length > 0) {
|
|
731
731
|
const buffer = Buffer.alloc(length);
|
|
732
732
|
await handle.read(buffer, 0, length, position);
|
|
733
|
-
position =
|
|
733
|
+
position = stat2.size;
|
|
734
734
|
process2.stdout.write(buffer.toString("utf-8"));
|
|
735
735
|
}
|
|
736
736
|
await handle.close();
|
|
@@ -1515,11 +1515,11 @@ function isNotFoundError(error) {
|
|
|
1515
1515
|
async function ensureOwnerOnlyDirectory(dirPath) {
|
|
1516
1516
|
if (shouldEnforceOwnerOnlyPermissions()) {
|
|
1517
1517
|
await fs2.mkdir(dirPath, { recursive: true, mode: OWNER_ONLY_DIRECTORY_MODE });
|
|
1518
|
-
const
|
|
1519
|
-
if (!
|
|
1518
|
+
const stat2 = await fs2.stat(dirPath);
|
|
1519
|
+
if (!stat2.isDirectory()) {
|
|
1520
1520
|
throw new Error(`Expected directory at ${dirPath}`);
|
|
1521
1521
|
}
|
|
1522
|
-
if (normalizeMode(
|
|
1522
|
+
if (normalizeMode(stat2.mode) !== OWNER_ONLY_DIRECTORY_MODE) {
|
|
1523
1523
|
await fs2.chmod(dirPath, OWNER_ONLY_DIRECTORY_MODE);
|
|
1524
1524
|
}
|
|
1525
1525
|
return;
|
|
@@ -1529,18 +1529,18 @@ async function ensureOwnerOnlyDirectory(dirPath) {
|
|
|
1529
1529
|
async function ensureOwnerOnlyFile(filePath) {
|
|
1530
1530
|
if (!shouldEnforceOwnerOnlyPermissions())
|
|
1531
1531
|
return;
|
|
1532
|
-
let
|
|
1532
|
+
let stat2;
|
|
1533
1533
|
try {
|
|
1534
|
-
|
|
1534
|
+
stat2 = await fs2.stat(filePath);
|
|
1535
1535
|
} catch (error) {
|
|
1536
1536
|
if (isNotFoundError(error))
|
|
1537
1537
|
return;
|
|
1538
1538
|
throw error;
|
|
1539
1539
|
}
|
|
1540
|
-
if (!
|
|
1540
|
+
if (!stat2.isFile()) {
|
|
1541
1541
|
throw new Error(`Expected regular file at ${filePath}`);
|
|
1542
1542
|
}
|
|
1543
|
-
if (normalizeMode(
|
|
1543
|
+
if (normalizeMode(stat2.mode) !== OWNER_ONLY_FILE_MODE) {
|
|
1544
1544
|
await fs2.chmod(filePath, OWNER_ONLY_FILE_MODE);
|
|
1545
1545
|
}
|
|
1546
1546
|
}
|
|
@@ -2824,8 +2824,8 @@ function expandHomePath(value, homeDir = os4.homedir()) {
|
|
|
2824
2824
|
function extractClaudeWrapperTarget(commandPath, options = {}) {
|
|
2825
2825
|
const { homeDir, maxClaudeWrapperBytes } = resolveOptions(options);
|
|
2826
2826
|
try {
|
|
2827
|
-
const
|
|
2828
|
-
if (!
|
|
2827
|
+
const stat2 = fsSync4.statSync(commandPath);
|
|
2828
|
+
if (!stat2.isFile() || stat2.size > maxClaudeWrapperBytes)
|
|
2829
2829
|
return null;
|
|
2830
2830
|
const raw = fsSync4.readFileSync(commandPath, "utf8");
|
|
2831
2831
|
const match = raw.match(/exec\s+(?:"([^"]+)"|'([^']+)'|([^\s]+))/);
|
|
@@ -2849,8 +2849,8 @@ function isBrokenClaudeWrapper(commandPath, options = {}) {
|
|
|
2849
2849
|
}
|
|
2850
2850
|
function isExecutableCandidate(candidate, platform = process.platform) {
|
|
2851
2851
|
try {
|
|
2852
|
-
const
|
|
2853
|
-
if (!
|
|
2852
|
+
const stat2 = fsSync4.statSync(candidate);
|
|
2853
|
+
if (!stat2.isFile())
|
|
2854
2854
|
return false;
|
|
2855
2855
|
if (platform === "win32")
|
|
2856
2856
|
return true;
|
|
@@ -9170,10 +9170,10 @@ function resolveGatewayRestartProbePath(entryPath, modulePath) {
|
|
|
9170
9170
|
}
|
|
9171
9171
|
function readRestartProbeFingerprint(filePath) {
|
|
9172
9172
|
try {
|
|
9173
|
-
const
|
|
9174
|
-
if (!
|
|
9173
|
+
const stat2 = fsSync8.statSync(filePath);
|
|
9174
|
+
if (!stat2.isFile())
|
|
9175
9175
|
return null;
|
|
9176
|
-
return `${Math.round(
|
|
9176
|
+
return `${Math.round(stat2.mtimeMs)}:${stat2.size}`;
|
|
9177
9177
|
} catch {
|
|
9178
9178
|
return null;
|
|
9179
9179
|
}
|
|
@@ -9861,9 +9861,27 @@ function createGatewayRuntimeState() {
|
|
|
9861
9861
|
}
|
|
9862
9862
|
|
|
9863
9863
|
// dist/managed-runtime.js
|
|
9864
|
-
import { randomUUID as
|
|
9864
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
9865
|
+
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
9865
9866
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
9866
9867
|
|
|
9868
|
+
// ../shared/dist/drive-transfer-policy.js
|
|
9869
|
+
var DRIVE_TRANSFER_POLICY = {
|
|
9870
|
+
inlineContentMaxBytes: 10 * 1024 * 1024,
|
|
9871
|
+
objectFileMaxBytes: 1024 * 1024 * 1024,
|
|
9872
|
+
directUploadThresholdBytes: 8 * 1024 * 1024,
|
|
9873
|
+
localBatchMaxChanges: 1e3,
|
|
9874
|
+
localBatchInlineUploadMaxBytes: 50 * 1024 * 1024,
|
|
9875
|
+
localSyncBatchUploadMaxBytes: 1024 * 1024 * 1024,
|
|
9876
|
+
signedRequestValidMs: 10 * 60 * 1e3,
|
|
9877
|
+
preparedUploadExpiresMs: 60 * 60 * 1e3,
|
|
9878
|
+
networkRequestTimeoutMs: 6e4,
|
|
9879
|
+
multipartUploadThresholdBytes: null
|
|
9880
|
+
};
|
|
9881
|
+
function getDriveLocalSyncFileTooLargeMessage(sizeBytes) {
|
|
9882
|
+
return `Local drive sync file upload size ${sizeBytes} exceeds the ${DRIVE_TRANSFER_POLICY.objectFileMaxBytes} byte limit`;
|
|
9883
|
+
}
|
|
9884
|
+
|
|
9867
9885
|
// ../shared/dist/linux-host-control/contract.js
|
|
9868
9886
|
var LINUX_HOST_CONTROL_ENDPOINT_PATH = "/functions/v1/linux-host-control";
|
|
9869
9887
|
function buildLinuxHostControlUrl(baseUrl) {
|
|
@@ -9887,6 +9905,14 @@ var DEFAULT_EXEC_TIMEOUT_MS = 3e5;
|
|
|
9887
9905
|
var DEFAULT_HEARTBEAT_INTERVAL_MS2 = 15e3;
|
|
9888
9906
|
var DEFAULT_MAX_CONSECUTIVE_HEARTBEAT_FAILURES = 3;
|
|
9889
9907
|
var DEFAULT_OUTPUT_CAPTURE_BYTES = 5e6;
|
|
9908
|
+
var DEFAULT_DRIVE_SYNC_ROOT = "/panorama/drives";
|
|
9909
|
+
var DEFAULT_DRIVE_SYNC_STATE_DIR = "/var/lib/panorama/drive-sync";
|
|
9910
|
+
var DEFAULT_DRIVE_SYNC_CHANGE_LIMIT = 50;
|
|
9911
|
+
var DEFAULT_DRIVE_SYNC_MAX_BATCHES_PER_TICK = 25;
|
|
9912
|
+
var DEFAULT_DRIVE_SYNC_DEBOUNCE_MS = 1e3;
|
|
9913
|
+
var DEFAULT_DRIVE_SYNC_POLL_INTERVAL_MS = 5e3;
|
|
9914
|
+
var DEFAULT_DRIVE_SYNC_LOCAL_AUDIT_INTERVAL_MS = 3e5;
|
|
9915
|
+
var DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS = DRIVE_TRANSFER_POLICY.networkRequestTimeoutMs;
|
|
9890
9916
|
function requireEnv(name, env = process16.env) {
|
|
9891
9917
|
const value = env[name]?.trim();
|
|
9892
9918
|
if (!value) {
|
|
@@ -9904,6 +9930,16 @@ function readPositiveInt(name, fallback, env = process16.env) {
|
|
|
9904
9930
|
}
|
|
9905
9931
|
return Math.floor(parsed);
|
|
9906
9932
|
}
|
|
9933
|
+
function readBoolean(name, fallback, env = process16.env) {
|
|
9934
|
+
const raw = env[name]?.trim().toLowerCase();
|
|
9935
|
+
if (!raw)
|
|
9936
|
+
return fallback;
|
|
9937
|
+
if (["1", "true", "yes", "on"].includes(raw))
|
|
9938
|
+
return true;
|
|
9939
|
+
if (["0", "false", "no", "off"].includes(raw))
|
|
9940
|
+
return false;
|
|
9941
|
+
throw new Error(`${name} must be a boolean`);
|
|
9942
|
+
}
|
|
9907
9943
|
function requirePositiveInt(name, env = process16.env) {
|
|
9908
9944
|
const raw = requireEnv(name, env);
|
|
9909
9945
|
const parsed = Number(raw);
|
|
@@ -9948,7 +9984,19 @@ function resolveManagedGatewayRuntimeConfig(env = process16.env) {
|
|
|
9948
9984
|
execTimeoutMs: readPositiveInt("PANORAMA_AGENT_EXEC_TIMEOUT_MS", DEFAULT_EXEC_TIMEOUT_MS, env),
|
|
9949
9985
|
outputCaptureBytes: readPositiveInt("PANORAMA_MANAGED_RUNTIME_OUTPUT_CAPTURE_BYTES", DEFAULT_OUTPUT_CAPTURE_BYTES, env),
|
|
9950
9986
|
heartbeatIntervalMs: readPositiveInt("PANORAMA_MANAGED_RUNTIME_HEARTBEAT_INTERVAL_MS", DEFAULT_HEARTBEAT_INTERVAL_MS2, env),
|
|
9951
|
-
maxConsecutiveHeartbeatFailures: readPositiveInt("PANORAMA_MANAGED_RUNTIME_MAX_CONSECUTIVE_HEARTBEAT_FAILURES", DEFAULT_MAX_CONSECUTIVE_HEARTBEAT_FAILURES, env)
|
|
9987
|
+
maxConsecutiveHeartbeatFailures: readPositiveInt("PANORAMA_MANAGED_RUNTIME_MAX_CONSECUTIVE_HEARTBEAT_FAILURES", DEFAULT_MAX_CONSECUTIVE_HEARTBEAT_FAILURES, env),
|
|
9988
|
+
driveSync: {
|
|
9989
|
+
enabled: readBoolean("PANORAMA_DRIVE_SYNC_ENABLED", false, env),
|
|
9990
|
+
rootDir: env.PANORAMA_DRIVE_SYNC_ROOT?.trim() || DEFAULT_DRIVE_SYNC_ROOT,
|
|
9991
|
+
stateDir: env.PANORAMA_DRIVE_SYNC_STATE_DIR?.trim() || DEFAULT_DRIVE_SYNC_STATE_DIR,
|
|
9992
|
+
clientKey: env.PANORAMA_DRIVE_SYNC_CLIENT_KEY?.trim() || null,
|
|
9993
|
+
changeLimit: readPositiveInt("PANORAMA_DRIVE_SYNC_CHANGE_LIMIT", DEFAULT_DRIVE_SYNC_CHANGE_LIMIT, env),
|
|
9994
|
+
maxBatchesPerTick: readPositiveInt("PANORAMA_DRIVE_SYNC_MAX_BATCHES_PER_TICK", DEFAULT_DRIVE_SYNC_MAX_BATCHES_PER_TICK, env),
|
|
9995
|
+
debounceMs: readPositiveInt("PANORAMA_DRIVE_SYNC_DEBOUNCE_MS", DEFAULT_DRIVE_SYNC_DEBOUNCE_MS, env),
|
|
9996
|
+
pollIntervalMs: readPositiveInt("PANORAMA_DRIVE_SYNC_POLL_INTERVAL_MS", DEFAULT_DRIVE_SYNC_POLL_INTERVAL_MS, env),
|
|
9997
|
+
localAuditIntervalMs: readPositiveInt("PANORAMA_DRIVE_SYNC_LOCAL_AUDIT_INTERVAL_MS", DEFAULT_DRIVE_SYNC_LOCAL_AUDIT_INTERVAL_MS, env),
|
|
9998
|
+
networkTimeoutMs: readPositiveInt("PANORAMA_DRIVE_SYNC_NETWORK_TIMEOUT_MS", DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS, env)
|
|
9999
|
+
}
|
|
9952
10000
|
};
|
|
9953
10001
|
}
|
|
9954
10002
|
|
|
@@ -10353,212 +10401,2977 @@ function startManagedRealtimeWakeLoop(params) {
|
|
|
10353
10401
|
};
|
|
10354
10402
|
}
|
|
10355
10403
|
|
|
10356
|
-
//
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10404
|
+
// dist/managed-runtime/drive-sync.js
|
|
10405
|
+
import { createHash as createHash3, randomUUID as randomUUID6 } from "node:crypto";
|
|
10406
|
+
import { mkdir as mkdir3, rename as rename3, rm as rm2 } from "node:fs/promises";
|
|
10407
|
+
import { hostname as hostname2 } from "node:os";
|
|
10408
|
+
import path18 from "node:path";
|
|
10409
|
+
import process17 from "node:process";
|
|
10410
|
+
|
|
10411
|
+
// dist/managed-runtime/drive-sync-filesystem.js
|
|
10412
|
+
import { createHash, randomUUID as randomUUID4 } from "node:crypto";
|
|
10413
|
+
import { createReadStream } from "node:fs";
|
|
10414
|
+
import { copyFile, lstat, mkdir, readdir, readlink, rename, stat } from "node:fs/promises";
|
|
10415
|
+
import path16 from "node:path";
|
|
10416
|
+
var DRIVE_SYNC_MAX_LOCAL_FILE_BYTES = DRIVE_TRANSFER_POLICY.objectFileMaxBytes;
|
|
10417
|
+
var UnstableLocalFileError = class extends Error {
|
|
10418
|
+
drivePath;
|
|
10419
|
+
constructor(drivePath) {
|
|
10420
|
+
super(`Local drive file changed while it was being read; deferring sync for ${drivePath}`);
|
|
10421
|
+
this.drivePath = drivePath;
|
|
10422
|
+
this.name = "UnstableLocalFileError";
|
|
10361
10423
|
}
|
|
10362
|
-
|
|
10363
|
-
|
|
10424
|
+
};
|
|
10425
|
+
function resolveDriveLocalRoot(rootDir, driveId) {
|
|
10426
|
+
if (!/^[0-9a-f-]{36}$/i.test(driveId)) {
|
|
10427
|
+
throw new Error(`Invalid drive id "${driveId}"`);
|
|
10428
|
+
}
|
|
10429
|
+
return path16.join(rootDir, driveId);
|
|
10430
|
+
}
|
|
10431
|
+
async function parkStaleDriveLocalFolder(rootDir, driveId) {
|
|
10432
|
+
const sourcePath = resolveDriveLocalRoot(rootDir, driveId);
|
|
10433
|
+
try {
|
|
10434
|
+
await lstat(sourcePath);
|
|
10435
|
+
} catch (error) {
|
|
10436
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10437
|
+
return null;
|
|
10438
|
+
}
|
|
10439
|
+
throw error;
|
|
10440
|
+
}
|
|
10441
|
+
const staleRoot = path16.join(path16.resolve(rootDir), ".stale");
|
|
10442
|
+
await mkdir(staleRoot, { recursive: true });
|
|
10443
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
10444
|
+
const targetPath = path16.join(staleRoot, `${driveId}-${timestamp}-${randomUUID4()}`);
|
|
10445
|
+
await rename(sourcePath, targetPath);
|
|
10446
|
+
return targetPath;
|
|
10447
|
+
}
|
|
10448
|
+
function resolveDriveLocalPath(driveRoot, drivePath) {
|
|
10449
|
+
const normalizedDrivePath = normalizeDrivePath(drivePath);
|
|
10450
|
+
const resolvedRoot = path16.resolve(driveRoot);
|
|
10451
|
+
if (normalizedDrivePath === "/") {
|
|
10452
|
+
return resolvedRoot;
|
|
10453
|
+
}
|
|
10454
|
+
const resolvedPath = path16.resolve(resolvedRoot, normalizedDrivePath.slice(1));
|
|
10455
|
+
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path16.sep}`)) {
|
|
10456
|
+
throw new Error(`Drive path escapes drive root: ${drivePath}`);
|
|
10457
|
+
}
|
|
10458
|
+
return resolvedPath;
|
|
10459
|
+
}
|
|
10460
|
+
function resolveDirtyDrivePathsForDrive(driveRoot, dirtyLocalPaths) {
|
|
10461
|
+
if (!dirtyLocalPaths) {
|
|
10364
10462
|
return null;
|
|
10365
10463
|
}
|
|
10366
|
-
const
|
|
10367
|
-
const
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10464
|
+
const resolvedRoot = path16.resolve(driveRoot);
|
|
10465
|
+
const dirtyDrivePaths = /* @__PURE__ */ new Set();
|
|
10466
|
+
for (const dirtyLocalPath of dirtyLocalPaths) {
|
|
10467
|
+
const resolvedDirtyPath = path16.resolve(dirtyLocalPath);
|
|
10468
|
+
const relativePath = path16.relative(resolvedRoot, resolvedDirtyPath);
|
|
10469
|
+
if (!relativePath) {
|
|
10470
|
+
return null;
|
|
10471
|
+
}
|
|
10472
|
+
if (relativePath.startsWith("..") || path16.isAbsolute(relativePath)) {
|
|
10473
|
+
continue;
|
|
10474
|
+
}
|
|
10475
|
+
dirtyDrivePaths.add(normalizeDrivePath(`/${relativePath.split(path16.sep).join("/")}`));
|
|
10476
|
+
}
|
|
10477
|
+
return dirtyDrivePaths;
|
|
10372
10478
|
}
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10479
|
+
async function readLocalDriveEntryAtPath(params) {
|
|
10480
|
+
const absolutePath = resolveDriveLocalPath(params.driveRoot, params.drivePath);
|
|
10481
|
+
let localStat;
|
|
10482
|
+
try {
|
|
10483
|
+
localStat = await lstat(absolutePath);
|
|
10484
|
+
} catch (error) {
|
|
10485
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10486
|
+
return null;
|
|
10487
|
+
}
|
|
10488
|
+
throw error;
|
|
10489
|
+
}
|
|
10490
|
+
if (localStat.isDirectory()) {
|
|
10491
|
+
return {
|
|
10492
|
+
driveId: params.driveId,
|
|
10493
|
+
path: normalizeDrivePath(params.drivePath),
|
|
10494
|
+
entryType: "directory",
|
|
10495
|
+
contentSha256: null,
|
|
10496
|
+
sizeBytes: null,
|
|
10497
|
+
mimeType: null,
|
|
10498
|
+
absolutePath
|
|
10499
|
+
};
|
|
10500
|
+
}
|
|
10501
|
+
if (localStat.isFile()) {
|
|
10502
|
+
return await readStableLocalFileEntry({
|
|
10503
|
+
driveId: params.driveId,
|
|
10504
|
+
drivePath: normalizeDrivePath(params.drivePath),
|
|
10505
|
+
absolutePath,
|
|
10506
|
+
initialStat: localStat
|
|
10391
10507
|
});
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
if (forceKillHandle) {
|
|
10405
|
-
clearTimeout(forceKillHandle);
|
|
10406
|
-
forceKillHandle = null;
|
|
10407
|
-
}
|
|
10408
|
-
options.registerCancellation?.(null);
|
|
10409
|
-
resolve(result);
|
|
10508
|
+
}
|
|
10509
|
+
if (localStat.isSymbolicLink()) {
|
|
10510
|
+
const linkTarget = await readlink(absolutePath).catch(() => "");
|
|
10511
|
+
return {
|
|
10512
|
+
driveId: params.driveId,
|
|
10513
|
+
path: normalizeDrivePath(params.drivePath),
|
|
10514
|
+
entryType: "symlink",
|
|
10515
|
+
contentSha256: sha256Hex2(Buffer.from(linkTarget)),
|
|
10516
|
+
sizeBytes: Buffer.byteLength(linkTarget),
|
|
10517
|
+
mimeType: null,
|
|
10518
|
+
absolutePath,
|
|
10519
|
+
unsupportedReason: "symlink_entry_unsupported"
|
|
10410
10520
|
};
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10521
|
+
}
|
|
10522
|
+
return {
|
|
10523
|
+
driveId: params.driveId,
|
|
10524
|
+
path: normalizeDrivePath(params.drivePath),
|
|
10525
|
+
entryType: "unsupported",
|
|
10526
|
+
contentSha256: null,
|
|
10527
|
+
sizeBytes: null,
|
|
10528
|
+
mimeType: null,
|
|
10529
|
+
absolutePath,
|
|
10530
|
+
unsupportedReason: describeUnsupportedLocalEntry(localStat)
|
|
10531
|
+
};
|
|
10532
|
+
}
|
|
10533
|
+
async function scanLocalDriveEntries(params) {
|
|
10534
|
+
const entries = /* @__PURE__ */ new Map();
|
|
10535
|
+
const deferredPaths = /* @__PURE__ */ new Set();
|
|
10536
|
+
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
10537
|
+
let deferredFileCount = 0;
|
|
10538
|
+
async function addAncestorDirectories(drivePath) {
|
|
10539
|
+
const normalizedPath = normalizeDrivePath(drivePath);
|
|
10540
|
+
const segments = normalizedPath.split("/").filter(Boolean);
|
|
10541
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
10542
|
+
const ancestorPath = normalizeDrivePath(`/${segments.slice(0, index).join("/")}`);
|
|
10543
|
+
if (entries.has(ancestorPath)) {
|
|
10544
|
+
continue;
|
|
10545
|
+
}
|
|
10546
|
+
const absolutePath = resolveDriveLocalPath(params.driveRoot, ancestorPath);
|
|
10414
10547
|
try {
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10548
|
+
const ancestorStat = await lstat(absolutePath);
|
|
10549
|
+
if (ancestorStat.isDirectory()) {
|
|
10550
|
+
entries.set(ancestorPath, {
|
|
10551
|
+
driveId: params.driveId,
|
|
10552
|
+
path: ancestorPath,
|
|
10553
|
+
entryType: "directory",
|
|
10554
|
+
contentSha256: null,
|
|
10555
|
+
sizeBytes: null,
|
|
10556
|
+
mimeType: null,
|
|
10557
|
+
absolutePath
|
|
10558
|
+
});
|
|
10420
10559
|
}
|
|
10560
|
+
} catch (error) {
|
|
10561
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10562
|
+
return;
|
|
10563
|
+
}
|
|
10564
|
+
throw error;
|
|
10421
10565
|
}
|
|
10422
|
-
}
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
}
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
});
|
|
10455
|
-
child.on("close", (code) => {
|
|
10456
|
-
const stdout = stdoutOutput.readText();
|
|
10457
|
-
const stderr = stderrOutput.readText();
|
|
10458
|
-
const durationMs = Date.now() - startedAt;
|
|
10459
|
-
const commonResult = {
|
|
10460
|
-
stdout,
|
|
10461
|
-
stderr,
|
|
10462
|
-
stdoutObservedBytes: stdoutOutput.observedBytes,
|
|
10463
|
-
stderrObservedBytes: stderrOutput.observedBytes,
|
|
10464
|
-
stdoutCapturedBytes: stdoutOutput.capturedBytes,
|
|
10465
|
-
stderrCapturedBytes: stderrOutput.capturedBytes,
|
|
10466
|
-
stdoutTruncated: stdoutOutput.truncated,
|
|
10467
|
-
stderrTruncated: stderrOutput.truncated,
|
|
10468
|
-
outputCaptureBytes,
|
|
10469
|
-
exitCode: code,
|
|
10470
|
-
durationMs
|
|
10471
|
-
};
|
|
10472
|
-
if (timedOut) {
|
|
10473
|
-
finish({
|
|
10474
|
-
...commonResult,
|
|
10475
|
-
status: "timed_out",
|
|
10476
|
-
error: `Command timed out after ${timeoutMs}ms`
|
|
10477
|
-
});
|
|
10478
|
-
return;
|
|
10479
|
-
}
|
|
10480
|
-
if (interrupted) {
|
|
10481
|
-
finish({
|
|
10482
|
-
...commonResult,
|
|
10483
|
-
status: "cancelled",
|
|
10484
|
-
error: "Command was interrupted because the managed gateway host was stopping."
|
|
10485
|
-
});
|
|
10486
|
-
return;
|
|
10487
|
-
}
|
|
10488
|
-
if (spawnError) {
|
|
10489
|
-
finish({
|
|
10490
|
-
...commonResult,
|
|
10491
|
-
status: "failed",
|
|
10492
|
-
error: spawnError
|
|
10493
|
-
});
|
|
10494
|
-
return;
|
|
10566
|
+
}
|
|
10567
|
+
}
|
|
10568
|
+
async function addPathEntry(absolutePath, drivePath, childStat) {
|
|
10569
|
+
await addAncestorDirectories(drivePath);
|
|
10570
|
+
if (childStat.isDirectory()) {
|
|
10571
|
+
entries.set(drivePath, {
|
|
10572
|
+
driveId: params.driveId,
|
|
10573
|
+
path: drivePath,
|
|
10574
|
+
entryType: "directory",
|
|
10575
|
+
contentSha256: null,
|
|
10576
|
+
sizeBytes: null,
|
|
10577
|
+
mimeType: null,
|
|
10578
|
+
absolutePath
|
|
10579
|
+
});
|
|
10580
|
+
await visitDirectory(absolutePath, drivePath);
|
|
10581
|
+
return;
|
|
10582
|
+
}
|
|
10583
|
+
if (childStat.isFile()) {
|
|
10584
|
+
try {
|
|
10585
|
+
entries.set(drivePath, await readStableLocalFileEntry({
|
|
10586
|
+
driveId: params.driveId,
|
|
10587
|
+
drivePath,
|
|
10588
|
+
absolutePath,
|
|
10589
|
+
initialStat: childStat
|
|
10590
|
+
}));
|
|
10591
|
+
} catch (error) {
|
|
10592
|
+
if (error instanceof UnstableLocalFileError) {
|
|
10593
|
+
deferredPaths.add(drivePath);
|
|
10594
|
+
deferredFileCount += 1;
|
|
10595
|
+
return;
|
|
10596
|
+
}
|
|
10597
|
+
throw error;
|
|
10495
10598
|
}
|
|
10496
|
-
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10599
|
+
return;
|
|
10600
|
+
}
|
|
10601
|
+
if (childStat.isSymbolicLink()) {
|
|
10602
|
+
const linkTarget = await readlink(absolutePath).catch(() => "");
|
|
10603
|
+
entries.set(drivePath, {
|
|
10604
|
+
driveId: params.driveId,
|
|
10605
|
+
path: drivePath,
|
|
10606
|
+
entryType: "symlink",
|
|
10607
|
+
contentSha256: sha256Hex2(Buffer.from(linkTarget)),
|
|
10608
|
+
sizeBytes: Buffer.byteLength(linkTarget),
|
|
10609
|
+
mimeType: null,
|
|
10610
|
+
absolutePath,
|
|
10611
|
+
unsupportedReason: "symlink_entry_unsupported"
|
|
10500
10612
|
});
|
|
10613
|
+
return;
|
|
10614
|
+
}
|
|
10615
|
+
entries.set(drivePath, {
|
|
10616
|
+
driveId: params.driveId,
|
|
10617
|
+
path: drivePath,
|
|
10618
|
+
entryType: "unsupported",
|
|
10619
|
+
contentSha256: null,
|
|
10620
|
+
sizeBytes: null,
|
|
10621
|
+
mimeType: null,
|
|
10622
|
+
absolutePath,
|
|
10623
|
+
unsupportedReason: describeUnsupportedLocalEntry(childStat)
|
|
10501
10624
|
});
|
|
10502
|
-
}
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
safeEnv[key] = value;
|
|
10625
|
+
}
|
|
10626
|
+
async function visitDirectory(absoluteDirectoryPath, driveDirectoryPath) {
|
|
10627
|
+
const resolvedDirectoryPath = path16.resolve(absoluteDirectoryPath);
|
|
10628
|
+
if (visitedDirectories.has(resolvedDirectoryPath)) {
|
|
10629
|
+
return;
|
|
10630
|
+
}
|
|
10631
|
+
visitedDirectories.add(resolvedDirectoryPath);
|
|
10632
|
+
const children = await readdir(absoluteDirectoryPath, { withFileTypes: true });
|
|
10633
|
+
for (const child of children) {
|
|
10634
|
+
const childAbsolutePath = path16.join(absoluteDirectoryPath, child.name);
|
|
10635
|
+
const childDrivePath = normalizeDrivePath(driveDirectoryPath === "/" ? `/${child.name}` : `${driveDirectoryPath}/${child.name}`);
|
|
10636
|
+
let childStat;
|
|
10637
|
+
try {
|
|
10638
|
+
childStat = await lstat(childAbsolutePath);
|
|
10639
|
+
} catch (error) {
|
|
10640
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10641
|
+
continue;
|
|
10642
|
+
}
|
|
10643
|
+
throw error;
|
|
10644
|
+
}
|
|
10645
|
+
await addPathEntry(childAbsolutePath, childDrivePath, childStat);
|
|
10524
10646
|
}
|
|
10525
10647
|
}
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
if (params.cycleId) {
|
|
10531
|
-
safeEnv.PANORAMA_AGENT_CYCLE_ID = params.cycleId;
|
|
10648
|
+
const rootDrivePaths = params.rootDrivePaths;
|
|
10649
|
+
if (!rootDrivePaths) {
|
|
10650
|
+
await visitDirectory(path16.resolve(params.driveRoot), "/");
|
|
10651
|
+
return { entries, deferredPaths, deferredFileCount };
|
|
10532
10652
|
}
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
}
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
const raw = await readFile(path17, "utf8");
|
|
10541
|
-
return parseControlSignalCandidate(JSON.parse(raw));
|
|
10542
|
-
} catch {
|
|
10543
|
-
return null;
|
|
10544
|
-
} finally {
|
|
10653
|
+
for (const dirtyPath of rootDrivePaths) {
|
|
10654
|
+
const normalizedDirtyPath = normalizeDrivePath(dirtyPath);
|
|
10655
|
+
if (normalizedDirtyPath === "/") {
|
|
10656
|
+
await visitDirectory(path16.resolve(params.driveRoot), "/");
|
|
10657
|
+
return { entries, deferredPaths, deferredFileCount };
|
|
10658
|
+
}
|
|
10659
|
+
const absolutePath = resolveDriveLocalPath(params.driveRoot, normalizedDirtyPath);
|
|
10545
10660
|
try {
|
|
10546
|
-
await
|
|
10547
|
-
|
|
10661
|
+
const childStat = await lstat(absolutePath);
|
|
10662
|
+
await addPathEntry(absolutePath, normalizedDirtyPath, childStat);
|
|
10663
|
+
} catch (error) {
|
|
10664
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10665
|
+
await addAncestorDirectories(normalizedDirtyPath);
|
|
10666
|
+
continue;
|
|
10667
|
+
}
|
|
10668
|
+
throw error;
|
|
10548
10669
|
}
|
|
10549
10670
|
}
|
|
10671
|
+
return { entries, deferredPaths, deferredFileCount };
|
|
10550
10672
|
}
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10673
|
+
async function writeLocalConflictCopy(params) {
|
|
10674
|
+
if (params.source.entryType !== "file") {
|
|
10675
|
+
throw new Error(`Cannot preserve non-file local conflict candidate: ${params.source.path}`);
|
|
10676
|
+
}
|
|
10677
|
+
const conflictDrivePath = await resolveAvailableConflictDrivePath({
|
|
10678
|
+
driveRoot: params.driveRoot,
|
|
10679
|
+
sourcePath: params.source.path,
|
|
10680
|
+
sourceRootPath: params.sourceRootPath,
|
|
10681
|
+
conflictRootPath: params.conflictRootPath
|
|
10682
|
+
});
|
|
10683
|
+
const conflictLocalPath = resolveDriveLocalPath(params.driveRoot, conflictDrivePath);
|
|
10684
|
+
await assertNoSymlinkInExistingPath(params.driveRoot, path16.dirname(conflictLocalPath));
|
|
10685
|
+
await mkdir(path16.dirname(conflictLocalPath), { recursive: true });
|
|
10686
|
+
await copyFile(params.source.absolutePath, conflictLocalPath);
|
|
10687
|
+
return conflictDrivePath;
|
|
10688
|
+
}
|
|
10689
|
+
async function resolveAvailableConflictDrivePath(params) {
|
|
10690
|
+
const parsed = path16.posix.parse(normalizeDrivePath(params.sourcePath));
|
|
10691
|
+
const conflictRoot = normalizeDrivePath(params.conflictRootPath);
|
|
10692
|
+
const sourceRoot = normalizeDrivePath(params.sourceRootPath);
|
|
10693
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
10694
|
+
const baseName = `${parsed.name} (Panorama conflict ${timestamp})`;
|
|
10695
|
+
const targetDirectory = sourceRoot === params.sourcePath ? parsed.dir || "/" : path16.posix.join(conflictRoot, path16.posix.relative(sourceRoot, parsed.dir || "/"));
|
|
10696
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
10697
|
+
const suffix = attempt === 0 ? "" : ` ${attempt + 1}`;
|
|
10698
|
+
const candidate = normalizeDrivePath(path16.posix.join(targetDirectory, `${baseName}${suffix}${parsed.ext}`));
|
|
10699
|
+
try {
|
|
10700
|
+
await lstat(resolveDriveLocalPath(params.driveRoot, candidate));
|
|
10701
|
+
} catch (error) {
|
|
10702
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10703
|
+
return candidate;
|
|
10704
|
+
}
|
|
10705
|
+
throw error;
|
|
10706
|
+
}
|
|
10707
|
+
}
|
|
10708
|
+
throw new Error(`Could not allocate a local conflict path for ${params.sourcePath}`);
|
|
10709
|
+
}
|
|
10710
|
+
function buildSubtreeConflictRootPath(deletedSubtreePath) {
|
|
10711
|
+
const parsed = path16.posix.parse(normalizeDrivePath(deletedSubtreePath));
|
|
10712
|
+
return normalizeDrivePath(path16.posix.join(parsed.dir || "/", `${parsed.base} (Panorama conflicts)`));
|
|
10713
|
+
}
|
|
10714
|
+
async function assertPathExists(filePath, errorMessage) {
|
|
10715
|
+
try {
|
|
10716
|
+
await stat(filePath);
|
|
10717
|
+
} catch {
|
|
10718
|
+
throw new Error(errorMessage);
|
|
10719
|
+
}
|
|
10720
|
+
}
|
|
10721
|
+
async function assertLocalPathIsNotSymlink(filePath, errorMessage) {
|
|
10722
|
+
try {
|
|
10723
|
+
const current = await lstat(filePath);
|
|
10724
|
+
if (current.isSymbolicLink()) {
|
|
10725
|
+
throw new Error(errorMessage);
|
|
10726
|
+
}
|
|
10727
|
+
} catch (error) {
|
|
10728
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10729
|
+
return;
|
|
10730
|
+
}
|
|
10731
|
+
throw error;
|
|
10732
|
+
}
|
|
10733
|
+
}
|
|
10734
|
+
async function assertNoSymlinkInExistingPath(driveRoot, targetPath) {
|
|
10735
|
+
const resolvedRoot = path16.resolve(driveRoot);
|
|
10736
|
+
const resolvedTarget = path16.resolve(targetPath);
|
|
10737
|
+
const relativePath = path16.relative(resolvedRoot, resolvedTarget);
|
|
10738
|
+
if (relativePath.startsWith("..") || path16.isAbsolute(relativePath)) {
|
|
10739
|
+
throw new Error(`Local drive path escapes drive root: ${targetPath}`);
|
|
10740
|
+
}
|
|
10741
|
+
if (!relativePath) {
|
|
10742
|
+
return;
|
|
10743
|
+
}
|
|
10744
|
+
let currentPath = resolvedRoot;
|
|
10745
|
+
for (const segment of relativePath.split(path16.sep).filter(Boolean)) {
|
|
10746
|
+
currentPath = path16.join(currentPath, segment);
|
|
10747
|
+
try {
|
|
10748
|
+
const current = await lstat(currentPath);
|
|
10749
|
+
if (current.isSymbolicLink()) {
|
|
10750
|
+
throw new Error(`Local drive path contains a symlink: ${currentPath}`);
|
|
10751
|
+
}
|
|
10752
|
+
} catch (error) {
|
|
10753
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
10754
|
+
return;
|
|
10755
|
+
}
|
|
10756
|
+
throw error;
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
function normalizeDrivePath(input) {
|
|
10761
|
+
if (!input.startsWith("/")) {
|
|
10762
|
+
throw new Error(`Drive path must be absolute: ${input}`);
|
|
10763
|
+
}
|
|
10764
|
+
const segments = input.split("/").filter(Boolean);
|
|
10765
|
+
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
10766
|
+
throw new Error(`Drive path must not contain traversal segments: ${input}`);
|
|
10767
|
+
}
|
|
10768
|
+
return input === "/" ? "/" : `/${segments.join("/")}`;
|
|
10769
|
+
}
|
|
10770
|
+
async function readStableLocalFileEntry(params) {
|
|
10771
|
+
const initialSizeBytes = Number(params.initialStat.size);
|
|
10772
|
+
const initialMtimeMs = Number(params.initialStat.mtimeMs);
|
|
10773
|
+
const initialCtimeMs = Number(params.initialStat.ctimeMs);
|
|
10774
|
+
if (initialSizeBytes > DRIVE_SYNC_MAX_LOCAL_FILE_BYTES) {
|
|
10775
|
+
return {
|
|
10776
|
+
driveId: params.driveId,
|
|
10777
|
+
path: params.drivePath,
|
|
10778
|
+
entryType: "unsupported",
|
|
10779
|
+
contentSha256: null,
|
|
10780
|
+
sizeBytes: initialSizeBytes,
|
|
10781
|
+
mimeType: null,
|
|
10782
|
+
absolutePath: params.absolutePath,
|
|
10783
|
+
mtimeMs: initialMtimeMs,
|
|
10784
|
+
ctimeMs: initialCtimeMs,
|
|
10785
|
+
unsupportedReason: `file_size_${initialSizeBytes}_exceeds_${DRIVE_SYNC_MAX_LOCAL_FILE_BYTES}_byte_limit`
|
|
10786
|
+
};
|
|
10787
|
+
}
|
|
10788
|
+
const contentSha256 = await sha256File(params.absolutePath);
|
|
10789
|
+
const finalStat = await lstat(params.absolutePath);
|
|
10790
|
+
if (!finalStat.isFile() || Number(finalStat.size) !== initialSizeBytes || Number(finalStat.mtimeMs) !== initialMtimeMs || Number(finalStat.ctimeMs) !== initialCtimeMs) {
|
|
10791
|
+
throw new UnstableLocalFileError(params.drivePath);
|
|
10792
|
+
}
|
|
10793
|
+
return {
|
|
10794
|
+
driveId: params.driveId,
|
|
10795
|
+
path: params.drivePath,
|
|
10796
|
+
entryType: "file",
|
|
10797
|
+
contentSha256,
|
|
10798
|
+
sizeBytes: Number(finalStat.size),
|
|
10799
|
+
mimeType: null,
|
|
10800
|
+
absolutePath: params.absolutePath,
|
|
10801
|
+
mtimeMs: Number(finalStat.mtimeMs),
|
|
10802
|
+
ctimeMs: Number(finalStat.ctimeMs)
|
|
10803
|
+
};
|
|
10804
|
+
}
|
|
10805
|
+
async function sha256File(filePath) {
|
|
10806
|
+
const hash = createHash("sha256");
|
|
10807
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
10808
|
+
hash.update(chunk);
|
|
10809
|
+
}
|
|
10810
|
+
return hash.digest("hex");
|
|
10811
|
+
}
|
|
10812
|
+
function describeUnsupportedLocalEntry(entry) {
|
|
10813
|
+
if (entry.isBlockDevice())
|
|
10814
|
+
return "block_device_unsupported";
|
|
10815
|
+
if (entry.isCharacterDevice())
|
|
10816
|
+
return "character_device_unsupported";
|
|
10817
|
+
if (entry.isFIFO())
|
|
10818
|
+
return "fifo_unsupported";
|
|
10819
|
+
if (entry.isSocket())
|
|
10820
|
+
return "socket_unsupported";
|
|
10821
|
+
return "unsupported_entry_type";
|
|
10822
|
+
}
|
|
10823
|
+
function sha256Hex2(bytes) {
|
|
10824
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
10825
|
+
}
|
|
10826
|
+
|
|
10827
|
+
// dist/managed-runtime/drive-sync-planner.js
|
|
10828
|
+
function planLocalDriveSync(params) {
|
|
10829
|
+
const conflictPlan = planLocalConflictActions(params);
|
|
10830
|
+
return {
|
|
10831
|
+
mutations: buildLocalDriveMutationsFromPlanState({
|
|
10832
|
+
...params,
|
|
10833
|
+
conflicts: conflictPlan.effectiveConflicts
|
|
10834
|
+
}),
|
|
10835
|
+
conflictActions: conflictPlan.actions
|
|
10836
|
+
};
|
|
10837
|
+
}
|
|
10838
|
+
function buildLocalDriveMutationsFromPlanState(params) {
|
|
10839
|
+
const mutations = [];
|
|
10840
|
+
const deletedDirectoryPaths = /* @__PURE__ */ new Set();
|
|
10841
|
+
const { moveByNewPath, movedBaselinePaths } = detectLocalFileMoves(params);
|
|
10842
|
+
const absentDeletedDirectoryRoots = /* @__PURE__ */ new Set();
|
|
10843
|
+
const localPaths = [...params.local.keys()].sort(compareDrivePathsForCreate);
|
|
10844
|
+
for (const drivePath of localPaths) {
|
|
10845
|
+
const localEntry = params.local.get(drivePath);
|
|
10846
|
+
const baselineEntry = params.baseline.get(drivePath) ?? null;
|
|
10847
|
+
const conflict = params.conflicts.get(drivePath) ?? null;
|
|
10848
|
+
if (baselineEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
|
|
10849
|
+
continue;
|
|
10850
|
+
}
|
|
10851
|
+
if (conflict && localEntryMatchesConflict(localEntry, conflict)) {
|
|
10852
|
+
continue;
|
|
10853
|
+
}
|
|
10854
|
+
const movedFromEntry = moveByNewPath.get(drivePath) ?? null;
|
|
10855
|
+
if (!baselineEntry && movedFromEntry) {
|
|
10856
|
+
mutations.push(buildMoveMutation(movedFromEntry, localEntry));
|
|
10857
|
+
continue;
|
|
10858
|
+
}
|
|
10859
|
+
if (baselineEntry && baselineEntry.entryType !== localEntry.entryType) {
|
|
10860
|
+
mutations.push(buildDeleteMutation(baselineEntry));
|
|
10861
|
+
if (baselineEntry.entryType === "directory") {
|
|
10862
|
+
deletedDirectoryPaths.add(baselineEntry.path);
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
10865
|
+
mutations.push(buildUpsertMutation(localEntry, baselineEntry));
|
|
10866
|
+
}
|
|
10867
|
+
for (const [drivePath, baselineEntry] of [...params.baseline].sort(([left], [right]) => compareDrivePathsForCreate(left, right))) {
|
|
10868
|
+
if (baselineEntry.entryType !== "directory") {
|
|
10869
|
+
continue;
|
|
10870
|
+
}
|
|
10871
|
+
if (params.local.has(drivePath)) {
|
|
10872
|
+
continue;
|
|
10873
|
+
}
|
|
10874
|
+
if (params.deferredPaths.has(drivePath)) {
|
|
10875
|
+
continue;
|
|
10876
|
+
}
|
|
10877
|
+
if (params.conflicts.has(drivePath)) {
|
|
10878
|
+
continue;
|
|
10879
|
+
}
|
|
10880
|
+
if (hasConflictInSubtree(drivePath, params.conflicts)) {
|
|
10881
|
+
continue;
|
|
10882
|
+
}
|
|
10883
|
+
if (movedBaselinePaths.has(drivePath)) {
|
|
10884
|
+
continue;
|
|
10885
|
+
}
|
|
10886
|
+
if (hasDeletedAncestor(drivePath, absentDeletedDirectoryRoots)) {
|
|
10887
|
+
continue;
|
|
10888
|
+
}
|
|
10889
|
+
absentDeletedDirectoryRoots.add(drivePath);
|
|
10890
|
+
}
|
|
10891
|
+
const baselinePaths = [...params.baseline.keys()].sort(compareDrivePathsForDelete);
|
|
10892
|
+
for (const drivePath of baselinePaths) {
|
|
10893
|
+
if (params.local.has(drivePath)) {
|
|
10894
|
+
continue;
|
|
10895
|
+
}
|
|
10896
|
+
if (params.deferredPaths.has(drivePath)) {
|
|
10897
|
+
continue;
|
|
10898
|
+
}
|
|
10899
|
+
if (params.conflicts.has(drivePath)) {
|
|
10900
|
+
continue;
|
|
10901
|
+
}
|
|
10902
|
+
if (movedBaselinePaths.has(drivePath)) {
|
|
10903
|
+
continue;
|
|
10904
|
+
}
|
|
10905
|
+
if (hasDeletedAncestor(drivePath, absentDeletedDirectoryRoots)) {
|
|
10906
|
+
continue;
|
|
10907
|
+
}
|
|
10908
|
+
if (hasDeletedAncestor(drivePath, deletedDirectoryPaths)) {
|
|
10909
|
+
continue;
|
|
10910
|
+
}
|
|
10911
|
+
const baselineEntry = params.baseline.get(drivePath);
|
|
10912
|
+
if (baselineEntry.entryType === "directory" && hasConflictInSubtree(drivePath, params.conflicts)) {
|
|
10913
|
+
continue;
|
|
10914
|
+
}
|
|
10915
|
+
mutations.push(buildDeleteMutation(baselineEntry));
|
|
10916
|
+
if (baselineEntry.entryType === "directory") {
|
|
10917
|
+
deletedDirectoryPaths.add(baselineEntry.path);
|
|
10918
|
+
}
|
|
10919
|
+
}
|
|
10920
|
+
return mutations;
|
|
10921
|
+
}
|
|
10922
|
+
function planLocalConflictActions(params) {
|
|
10923
|
+
const actions = [];
|
|
10924
|
+
const effectiveConflicts = new Map(params.conflicts);
|
|
10925
|
+
for (const conflict of [...params.conflicts.values()].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
10926
|
+
const baselineEntry = params.baseline.get(conflict.path) ?? null;
|
|
10927
|
+
const canonicalEntry = params.local.get(conflict.path) ?? null;
|
|
10928
|
+
const candidateEntry = conflict.conflictPath ? params.local.get(conflict.conflictPath) ?? null : null;
|
|
10929
|
+
const canonicalMatchesSynced = baselineEntry !== null && canonicalEntry !== null && localEntryMatchesBaseline(canonicalEntry, baselineEntry);
|
|
10930
|
+
if (canonicalMatchesSynced && candidateEntry !== null && conflict.conflictPath) {
|
|
10931
|
+
const candidateBaselineEntry = params.baseline.get(conflict.conflictPath) ?? null;
|
|
10932
|
+
if (candidateBaselineEntry !== null && localEntryMatchesBaseline(candidateEntry, candidateBaselineEntry)) {
|
|
10933
|
+
actions.push({
|
|
10934
|
+
type: "clear_conflict",
|
|
10935
|
+
driveId: conflict.driveId,
|
|
10936
|
+
path: conflict.path,
|
|
10937
|
+
reason: "candidate_preserved"
|
|
10938
|
+
});
|
|
10939
|
+
effectiveConflicts.delete(conflict.path);
|
|
10940
|
+
continue;
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10943
|
+
if (candidateEntry !== null) {
|
|
10944
|
+
continue;
|
|
10945
|
+
}
|
|
10946
|
+
const movedCandidatePath = canonicalMatchesSynced ? findUniqueMovedConflictCandidatePath(params, conflict) : null;
|
|
10947
|
+
if (movedCandidatePath) {
|
|
10948
|
+
actions.push({
|
|
10949
|
+
type: "update_conflict_path",
|
|
10950
|
+
driveId: conflict.driveId,
|
|
10951
|
+
path: conflict.path,
|
|
10952
|
+
conflictPath: movedCandidatePath,
|
|
10953
|
+
reason: "candidate_moved"
|
|
10954
|
+
});
|
|
10955
|
+
effectiveConflicts.set(conflict.path, {
|
|
10956
|
+
...conflict,
|
|
10957
|
+
conflictPath: movedCandidatePath
|
|
10958
|
+
});
|
|
10959
|
+
continue;
|
|
10960
|
+
}
|
|
10961
|
+
if (canonicalMatchesSynced || baselineEntry === null && canonicalEntry === null) {
|
|
10962
|
+
actions.push({
|
|
10963
|
+
type: "clear_conflict",
|
|
10964
|
+
driveId: conflict.driveId,
|
|
10965
|
+
path: conflict.path,
|
|
10966
|
+
reason: "candidate_removed"
|
|
10967
|
+
});
|
|
10968
|
+
effectiveConflicts.delete(conflict.path);
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10971
|
+
return { actions, effectiveConflicts };
|
|
10972
|
+
}
|
|
10973
|
+
function findUniqueMovedConflictCandidatePath(params, conflict) {
|
|
10974
|
+
const key = conflictCandidateMoveKey(conflict);
|
|
10975
|
+
if (!key) {
|
|
10976
|
+
return null;
|
|
10977
|
+
}
|
|
10978
|
+
let matchedPath = null;
|
|
10979
|
+
for (const [drivePath, localEntry] of params.local) {
|
|
10980
|
+
if (drivePath === conflict.path || drivePath === conflict.conflictPath) {
|
|
10981
|
+
continue;
|
|
10982
|
+
}
|
|
10983
|
+
if (params.baseline.has(drivePath) || params.conflicts.has(drivePath)) {
|
|
10984
|
+
continue;
|
|
10985
|
+
}
|
|
10986
|
+
if (localFileMoveKey(localEntry) !== key) {
|
|
10987
|
+
continue;
|
|
10988
|
+
}
|
|
10989
|
+
if (matchedPath !== null) {
|
|
10990
|
+
return null;
|
|
10991
|
+
}
|
|
10992
|
+
matchedPath = drivePath;
|
|
10993
|
+
}
|
|
10994
|
+
return matchedPath;
|
|
10995
|
+
}
|
|
10996
|
+
function conflictCandidateMoveKey(conflict) {
|
|
10997
|
+
if (!conflict.localContentSha256 || conflict.localSizeBytes === null) {
|
|
10998
|
+
return null;
|
|
10999
|
+
}
|
|
11000
|
+
return `${conflict.localContentSha256}:${conflict.localSizeBytes}`;
|
|
11001
|
+
}
|
|
11002
|
+
function detectLocalFileMoves(params) {
|
|
11003
|
+
const deletedBaselineByKey = /* @__PURE__ */ new Map();
|
|
11004
|
+
const newLocalByKey = /* @__PURE__ */ new Map();
|
|
11005
|
+
for (const [drivePath, baselineEntry] of params.baseline) {
|
|
11006
|
+
if (params.local.has(drivePath) || params.deferredPaths.has(drivePath) || params.conflicts.has(drivePath)) {
|
|
11007
|
+
continue;
|
|
11008
|
+
}
|
|
11009
|
+
const key = localFileMoveKey(baselineEntry);
|
|
11010
|
+
if (!key) {
|
|
11011
|
+
continue;
|
|
11012
|
+
}
|
|
11013
|
+
deletedBaselineByKey.set(key, deletedBaselineByKey.has(key) ? null : baselineEntry);
|
|
11014
|
+
}
|
|
11015
|
+
for (const [drivePath, localEntry] of params.local) {
|
|
11016
|
+
if (params.baseline.has(drivePath) || params.conflicts.has(drivePath)) {
|
|
11017
|
+
continue;
|
|
11018
|
+
}
|
|
11019
|
+
const key = localFileMoveKey(localEntry);
|
|
11020
|
+
if (!key) {
|
|
11021
|
+
continue;
|
|
11022
|
+
}
|
|
11023
|
+
newLocalByKey.set(key, newLocalByKey.has(key) ? null : localEntry);
|
|
11024
|
+
}
|
|
11025
|
+
const moveByNewPath = /* @__PURE__ */ new Map();
|
|
11026
|
+
const movedBaselinePaths = /* @__PURE__ */ new Set();
|
|
11027
|
+
for (const [key, baselineEntry] of deletedBaselineByKey) {
|
|
11028
|
+
const localEntry = newLocalByKey.get(key) ?? null;
|
|
11029
|
+
if (!baselineEntry || !localEntry) {
|
|
11030
|
+
continue;
|
|
11031
|
+
}
|
|
11032
|
+
moveByNewPath.set(localEntry.path, baselineEntry);
|
|
11033
|
+
movedBaselinePaths.add(baselineEntry.path);
|
|
11034
|
+
}
|
|
11035
|
+
return { moveByNewPath, movedBaselinePaths };
|
|
11036
|
+
}
|
|
11037
|
+
function localFileMoveKey(entry) {
|
|
11038
|
+
if (entry.entryType !== "file" || !entry.contentSha256 || entry.sizeBytes === null) {
|
|
11039
|
+
return null;
|
|
11040
|
+
}
|
|
11041
|
+
return `${entry.contentSha256}:${entry.sizeBytes}`;
|
|
11042
|
+
}
|
|
11043
|
+
function buildUpsertMutation(localEntry, baselineEntry) {
|
|
11044
|
+
if (localEntry.entryType === "directory") {
|
|
11045
|
+
return {
|
|
11046
|
+
request: {
|
|
11047
|
+
type: "mkdir",
|
|
11048
|
+
path: localEntry.path
|
|
11049
|
+
},
|
|
11050
|
+
stateEntry: toStateEntry(localEntry, null),
|
|
11051
|
+
uploadBytes: 0
|
|
11052
|
+
};
|
|
11053
|
+
}
|
|
11054
|
+
if (localEntry.entryType === "file") {
|
|
11055
|
+
if (!localEntry.contentSha256 || localEntry.sizeBytes === null) {
|
|
11056
|
+
throw new Error(`Local drive file scan is missing content metadata: ${localEntry.path}`);
|
|
11057
|
+
}
|
|
11058
|
+
return {
|
|
11059
|
+
request: {
|
|
11060
|
+
type: "write_file",
|
|
11061
|
+
path: localEntry.path,
|
|
11062
|
+
content_sha256: localEntry.contentSha256,
|
|
11063
|
+
size_bytes: localEntry.sizeBytes,
|
|
11064
|
+
mime_type: localEntry.mimeType,
|
|
11065
|
+
base_version_id: baselineEntry?.entryType === "file" ? baselineEntry.versionId : null
|
|
11066
|
+
},
|
|
11067
|
+
stateEntry: toStateEntry(localEntry, baselineEntry?.entryType === "file" ? baselineEntry.versionId : null),
|
|
11068
|
+
uploadBytes: localEntry.sizeBytes ?? 0,
|
|
11069
|
+
localFilePath: localEntry.absolutePath,
|
|
11070
|
+
localFingerprint: localEntry.mtimeMs === void 0 || localEntry.ctimeMs === void 0 ? void 0 : {
|
|
11071
|
+
sizeBytes: localEntry.sizeBytes,
|
|
11072
|
+
mtimeMs: localEntry.mtimeMs,
|
|
11073
|
+
ctimeMs: localEntry.ctimeMs
|
|
11074
|
+
}
|
|
11075
|
+
};
|
|
11076
|
+
}
|
|
11077
|
+
return {
|
|
11078
|
+
request: {
|
|
11079
|
+
type: localEntry.entryType === "symlink" ? "symlink" : "unsupported",
|
|
11080
|
+
path: localEntry.path,
|
|
11081
|
+
entry_type: localEntry.entryType,
|
|
11082
|
+
reason: localEntry.unsupportedReason ?? "unsupported_entry_type"
|
|
11083
|
+
},
|
|
11084
|
+
stateEntry: toStateEntry(localEntry, baselineEntry?.versionId ?? null),
|
|
11085
|
+
uploadBytes: 0
|
|
11086
|
+
};
|
|
11087
|
+
}
|
|
11088
|
+
function buildMoveMutation(fromEntry, toEntry) {
|
|
11089
|
+
return {
|
|
11090
|
+
request: {
|
|
11091
|
+
type: "move",
|
|
11092
|
+
from_path: fromEntry.path,
|
|
11093
|
+
to_path: toEntry.path,
|
|
11094
|
+
base_version_id: fromEntry.entryType === "file" ? fromEntry.versionId : null
|
|
11095
|
+
},
|
|
11096
|
+
stateEntry: toStateEntry(toEntry, fromEntry.versionId),
|
|
11097
|
+
uploadBytes: 0
|
|
11098
|
+
};
|
|
11099
|
+
}
|
|
11100
|
+
function buildDeleteMutation(entry) {
|
|
11101
|
+
return {
|
|
11102
|
+
request: {
|
|
11103
|
+
type: "delete",
|
|
11104
|
+
path: entry.path,
|
|
11105
|
+
base_version_id: entry.entryType === "file" ? entry.versionId : null
|
|
11106
|
+
},
|
|
11107
|
+
stateEntry: entry,
|
|
11108
|
+
uploadBytes: 0
|
|
11109
|
+
};
|
|
11110
|
+
}
|
|
11111
|
+
function toStateEntry(localEntry, versionId) {
|
|
11112
|
+
return {
|
|
11113
|
+
driveId: localEntry.driveId,
|
|
11114
|
+
path: localEntry.path,
|
|
11115
|
+
entryType: localEntry.entryType,
|
|
11116
|
+
versionId,
|
|
11117
|
+
contentSha256: localEntry.contentSha256,
|
|
11118
|
+
sizeBytes: localEntry.sizeBytes,
|
|
11119
|
+
mimeType: localEntry.mimeType
|
|
11120
|
+
};
|
|
11121
|
+
}
|
|
11122
|
+
function localEntryMatchesBaseline(localEntry, baselineEntry) {
|
|
11123
|
+
return localEntry.entryType === baselineEntry.entryType && localEntry.contentSha256 === baselineEntry.contentSha256 && localEntry.sizeBytes === baselineEntry.sizeBytes;
|
|
11124
|
+
}
|
|
11125
|
+
function localEntryMatchesConflict(localEntry, conflict) {
|
|
11126
|
+
return localEntry.contentSha256 === conflict.localContentSha256 && localEntry.sizeBytes === conflict.localSizeBytes;
|
|
11127
|
+
}
|
|
11128
|
+
function compareDrivePathsForCreate(left, right) {
|
|
11129
|
+
const leftDepth = drivePathDepth(left);
|
|
11130
|
+
const rightDepth = drivePathDepth(right);
|
|
11131
|
+
if (leftDepth !== rightDepth) {
|
|
11132
|
+
return leftDepth - rightDepth;
|
|
11133
|
+
}
|
|
11134
|
+
return left.localeCompare(right);
|
|
11135
|
+
}
|
|
11136
|
+
function compareDrivePathsForDelete(left, right) {
|
|
11137
|
+
const leftDepth = drivePathDepth(left);
|
|
11138
|
+
const rightDepth = drivePathDepth(right);
|
|
11139
|
+
if (leftDepth !== rightDepth) {
|
|
11140
|
+
return rightDepth - leftDepth;
|
|
11141
|
+
}
|
|
11142
|
+
return right.localeCompare(left);
|
|
11143
|
+
}
|
|
11144
|
+
function drivePathDepth(drivePath) {
|
|
11145
|
+
return drivePath.split("/").filter(Boolean).length;
|
|
11146
|
+
}
|
|
11147
|
+
function hasDeletedAncestor(drivePath, deletedDirectoryPaths) {
|
|
11148
|
+
for (const deletedDirectoryPath of deletedDirectoryPaths) {
|
|
11149
|
+
if (deletedDirectoryPath === "/") {
|
|
11150
|
+
return true;
|
|
11151
|
+
}
|
|
11152
|
+
if (drivePath.startsWith(`${deletedDirectoryPath}/`)) {
|
|
11153
|
+
return true;
|
|
11154
|
+
}
|
|
11155
|
+
}
|
|
11156
|
+
return false;
|
|
11157
|
+
}
|
|
11158
|
+
function hasConflictInSubtree(drivePath, conflicts) {
|
|
11159
|
+
for (const conflictPath of conflicts.keys()) {
|
|
11160
|
+
if (drivePathIsInSubtree(conflictPath, drivePath)) {
|
|
11161
|
+
return true;
|
|
11162
|
+
}
|
|
11163
|
+
}
|
|
11164
|
+
return false;
|
|
11165
|
+
}
|
|
11166
|
+
function drivePathIsInSubtree(drivePath, subtreeRoot) {
|
|
11167
|
+
if (subtreeRoot === "/") {
|
|
11168
|
+
return true;
|
|
11169
|
+
}
|
|
11170
|
+
return drivePath === subtreeRoot || drivePath.startsWith(`${subtreeRoot}/`);
|
|
11171
|
+
}
|
|
11172
|
+
|
|
11173
|
+
// dist/managed-runtime/drive-sync-remote-planner.js
|
|
11174
|
+
function planRemoteDriveBatch(batch) {
|
|
11175
|
+
const operations = [];
|
|
11176
|
+
for (const change of batch.paths.filter((pathChange) => pathChange.change_type === "moved_to")) {
|
|
11177
|
+
if (!change.from_path || !change.to_path) {
|
|
11178
|
+
throw new Error(`Drive move batch ${batch.id} is missing from_path or to_path`);
|
|
11179
|
+
}
|
|
11180
|
+
operations.push({
|
|
11181
|
+
type: "move",
|
|
11182
|
+
change,
|
|
11183
|
+
fromPath: change.from_path,
|
|
11184
|
+
toPath: change.to_path,
|
|
11185
|
+
preserve: [
|
|
11186
|
+
{
|
|
11187
|
+
type: "path_conflict_candidate",
|
|
11188
|
+
path: change.from_path,
|
|
11189
|
+
reason: "remote_move_source_conflict"
|
|
11190
|
+
},
|
|
11191
|
+
{
|
|
11192
|
+
type: "overwrite_candidates",
|
|
11193
|
+
path: change.to_path,
|
|
11194
|
+
reason: "remote_move_target_conflict"
|
|
11195
|
+
}
|
|
11196
|
+
]
|
|
11197
|
+
});
|
|
11198
|
+
}
|
|
11199
|
+
for (const change of batch.paths) {
|
|
11200
|
+
if (change.change_type === "moved_from" || change.change_type === "moved_to") {
|
|
11201
|
+
continue;
|
|
11202
|
+
}
|
|
11203
|
+
if (change.change_type === "unsupported" || change.change_type === "conflicted") {
|
|
11204
|
+
continue;
|
|
11205
|
+
}
|
|
11206
|
+
if (change.change_type === "deleted") {
|
|
11207
|
+
operations.push({
|
|
11208
|
+
type: "delete",
|
|
11209
|
+
change,
|
|
11210
|
+
path: change.path,
|
|
11211
|
+
preserve: [
|
|
11212
|
+
{
|
|
11213
|
+
type: "overwrite_candidates",
|
|
11214
|
+
path: change.path,
|
|
11215
|
+
reason: "remote_delete_conflict",
|
|
11216
|
+
subtreeReason: "remote_delete_subtree_conflict"
|
|
11217
|
+
}
|
|
11218
|
+
]
|
|
11219
|
+
});
|
|
11220
|
+
continue;
|
|
11221
|
+
}
|
|
11222
|
+
if (change.entry_type === "directory") {
|
|
11223
|
+
operations.push({
|
|
11224
|
+
type: "mkdir",
|
|
11225
|
+
change,
|
|
11226
|
+
path: change.path
|
|
11227
|
+
});
|
|
11228
|
+
continue;
|
|
11229
|
+
}
|
|
11230
|
+
if (change.entry_type !== "file" || !change.version_id) {
|
|
11231
|
+
throw new Error(`Drive file change ${change.id} is missing a file version`);
|
|
11232
|
+
}
|
|
11233
|
+
operations.push({
|
|
11234
|
+
type: "write_file",
|
|
11235
|
+
change,
|
|
11236
|
+
path: change.path,
|
|
11237
|
+
versionId: change.version_id,
|
|
11238
|
+
preserve: [
|
|
11239
|
+
{
|
|
11240
|
+
type: "overwrite_candidates",
|
|
11241
|
+
path: change.path,
|
|
11242
|
+
reason: "remote_write_conflict",
|
|
11243
|
+
subtreeReason: "remote_write_directory_target_conflict"
|
|
11244
|
+
}
|
|
11245
|
+
]
|
|
11246
|
+
});
|
|
11247
|
+
}
|
|
11248
|
+
return operations;
|
|
11249
|
+
}
|
|
11250
|
+
|
|
11251
|
+
// dist/managed-runtime/drive-sync-state-applier.js
|
|
11252
|
+
function applyLocalDriveConflictActions(params) {
|
|
11253
|
+
for (const action of params.actions) {
|
|
11254
|
+
if (action.type === "clear_conflict") {
|
|
11255
|
+
params.stateStore.deleteConflict?.(action.driveId, action.path);
|
|
11256
|
+
continue;
|
|
11257
|
+
}
|
|
11258
|
+
const existingConflict = params.stateStore.getConflict?.(action.driveId, action.path) ?? null;
|
|
11259
|
+
if (!existingConflict) {
|
|
11260
|
+
continue;
|
|
11261
|
+
}
|
|
11262
|
+
params.stateStore.upsertConflict?.({
|
|
11263
|
+
...existingConflict,
|
|
11264
|
+
conflictPath: action.conflictPath
|
|
11265
|
+
});
|
|
11266
|
+
}
|
|
11267
|
+
}
|
|
11268
|
+
function applyAppliedLocalDriveMutationState(params) {
|
|
11269
|
+
if (params.mutation.request.type === "move") {
|
|
11270
|
+
const fromPath = readMutationPath(params.mutation.request, "from_path");
|
|
11271
|
+
const toPath = readMutationPath(params.mutation.request, "to_path");
|
|
11272
|
+
params.stateStore.moveEntryTree(params.driveId, fromPath, toPath);
|
|
11273
|
+
params.stateStore.deleteConflictTree?.(params.driveId, fromPath);
|
|
11274
|
+
params.stateStore.deleteConflictTree?.(params.driveId, toPath);
|
|
11275
|
+
return;
|
|
11276
|
+
}
|
|
11277
|
+
if (params.mutation.request.type === "delete") {
|
|
11278
|
+
const drivePath = readMutationPath(params.mutation.request, "path");
|
|
11279
|
+
params.stateStore.deleteEntryTree(params.driveId, drivePath);
|
|
11280
|
+
params.stateStore.deleteConflictTree?.(params.driveId, drivePath);
|
|
11281
|
+
return;
|
|
11282
|
+
}
|
|
11283
|
+
if (!params.mutation.stateEntry) {
|
|
11284
|
+
return;
|
|
11285
|
+
}
|
|
11286
|
+
params.stateStore.upsertEntry({
|
|
11287
|
+
...params.mutation.stateEntry,
|
|
11288
|
+
versionId: typeof params.result.version_id === "string" ? params.result.version_id : params.mutation.stateEntry.versionId
|
|
11289
|
+
});
|
|
11290
|
+
params.stateStore.deleteConflict?.(params.driveId, params.mutation.stateEntry.path);
|
|
11291
|
+
}
|
|
11292
|
+
function applyUnsupportedLocalDriveMutationState(params) {
|
|
11293
|
+
if (!params.mutation.stateEntry) {
|
|
11294
|
+
return;
|
|
11295
|
+
}
|
|
11296
|
+
params.stateStore.upsertEntry(params.mutation.stateEntry);
|
|
11297
|
+
params.stateStore.deleteConflict?.(params.driveId, params.mutation.stateEntry.path);
|
|
11298
|
+
}
|
|
11299
|
+
function recordLocalUploadConflict(params) {
|
|
11300
|
+
params.stateStore.upsertConflict?.({
|
|
11301
|
+
driveId: params.driveId,
|
|
11302
|
+
path: params.conflictPath,
|
|
11303
|
+
conflictType: "local_upload",
|
|
11304
|
+
reason: params.result.reason ?? null,
|
|
11305
|
+
localContentSha256: params.mutation.stateEntry?.contentSha256 ?? null,
|
|
11306
|
+
localSizeBytes: params.mutation.stateEntry?.sizeBytes ?? null,
|
|
11307
|
+
localVersionId: typeof params.result.version_id === "string" ? params.result.version_id : params.mutation.stateEntry?.versionId ?? null,
|
|
11308
|
+
conflictId: typeof params.result.conflict_id === "string" ? params.result.conflict_id : null,
|
|
11309
|
+
conflictPath: params.localConflictPath,
|
|
11310
|
+
remoteBatchId: null,
|
|
11311
|
+
remoteSequence: null
|
|
11312
|
+
});
|
|
11313
|
+
}
|
|
11314
|
+
function applyRemoteDriveMoveState(params) {
|
|
11315
|
+
params.stateStore.moveEntryTree(params.driveId, normalizeDrivePath2(params.fromPath), normalizeDrivePath2(params.toPath));
|
|
11316
|
+
}
|
|
11317
|
+
function applyRemoteDriveDeleteState(params) {
|
|
11318
|
+
params.stateStore.deleteEntryTree(params.driveId, normalizeDrivePath2(params.path));
|
|
11319
|
+
}
|
|
11320
|
+
function applyRemoteDriveDirectoryState(params) {
|
|
11321
|
+
params.stateStore.upsertEntry({
|
|
11322
|
+
driveId: params.driveId,
|
|
11323
|
+
path: normalizeDrivePath2(params.path),
|
|
11324
|
+
entryType: "directory",
|
|
11325
|
+
versionId: null,
|
|
11326
|
+
contentSha256: null,
|
|
11327
|
+
sizeBytes: null,
|
|
11328
|
+
mimeType: null
|
|
11329
|
+
});
|
|
11330
|
+
}
|
|
11331
|
+
function applyRemoteDriveFileState(params) {
|
|
11332
|
+
params.stateStore.upsertEntry({
|
|
11333
|
+
driveId: params.driveId,
|
|
11334
|
+
path: normalizeDrivePath2(params.path),
|
|
11335
|
+
entryType: "file",
|
|
11336
|
+
versionId: params.versionId,
|
|
11337
|
+
contentSha256: params.version?.content_sha256 ?? null,
|
|
11338
|
+
sizeBytes: params.version?.size_bytes ?? params.downloadedSizeBytes,
|
|
11339
|
+
mimeType: params.version?.mime_type ?? null
|
|
11340
|
+
});
|
|
11341
|
+
}
|
|
11342
|
+
function recordRemoteApplyConflict(params) {
|
|
11343
|
+
const conflict = {
|
|
11344
|
+
driveId: params.driveId,
|
|
11345
|
+
path: normalizeDrivePath2(params.path),
|
|
11346
|
+
conflictType: "remote_apply",
|
|
11347
|
+
reason: params.reason,
|
|
11348
|
+
localContentSha256: params.localEntry?.contentSha256 ?? null,
|
|
11349
|
+
localSizeBytes: params.localEntry?.sizeBytes ?? null,
|
|
11350
|
+
localVersionId: params.baselineEntry?.versionId ?? null,
|
|
11351
|
+
conflictId: null,
|
|
11352
|
+
conflictPath: params.conflictPath,
|
|
11353
|
+
remoteBatchId: params.remoteBatchId,
|
|
11354
|
+
remoteSequence: params.remoteSequence
|
|
11355
|
+
};
|
|
11356
|
+
params.stateStore.upsertConflict?.(conflict);
|
|
11357
|
+
return conflict;
|
|
11358
|
+
}
|
|
11359
|
+
function resolveLocalMutationResultPath(result, mutation) {
|
|
11360
|
+
if (typeof result.path === "string") {
|
|
11361
|
+
return normalizeDrivePath2(result.path);
|
|
11362
|
+
}
|
|
11363
|
+
if (typeof result.to_path === "string") {
|
|
11364
|
+
return normalizeDrivePath2(result.to_path);
|
|
11365
|
+
}
|
|
11366
|
+
if (typeof result.from_path === "string") {
|
|
11367
|
+
return normalizeDrivePath2(result.from_path);
|
|
11368
|
+
}
|
|
11369
|
+
if (mutation.stateEntry) {
|
|
11370
|
+
return mutation.stateEntry.path;
|
|
11371
|
+
}
|
|
11372
|
+
if (typeof mutation.request.path === "string") {
|
|
11373
|
+
return normalizeDrivePath2(mutation.request.path);
|
|
11374
|
+
}
|
|
11375
|
+
if (typeof mutation.request.to_path === "string") {
|
|
11376
|
+
return normalizeDrivePath2(mutation.request.to_path);
|
|
11377
|
+
}
|
|
11378
|
+
if (typeof mutation.request.from_path === "string") {
|
|
11379
|
+
return normalizeDrivePath2(mutation.request.from_path);
|
|
11380
|
+
}
|
|
11381
|
+
throw new Error("Local drive sync mutation result did not include a path");
|
|
11382
|
+
}
|
|
11383
|
+
function readMutationPath(request, key) {
|
|
11384
|
+
const value = request[key];
|
|
11385
|
+
if (typeof value !== "string") {
|
|
11386
|
+
throw new Error(`Local drive sync mutation is missing ${key}`);
|
|
11387
|
+
}
|
|
11388
|
+
return normalizeDrivePath2(value);
|
|
11389
|
+
}
|
|
11390
|
+
function normalizeDrivePath2(input) {
|
|
11391
|
+
if (!input.startsWith("/")) {
|
|
11392
|
+
throw new Error(`Drive path must be absolute: ${input}`);
|
|
11393
|
+
}
|
|
11394
|
+
const segments = input.split("/").filter(Boolean);
|
|
11395
|
+
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
11396
|
+
throw new Error(`Drive path must not contain traversal segments: ${input}`);
|
|
11397
|
+
}
|
|
11398
|
+
return input === "/" ? "/" : `/${segments.join("/")}`;
|
|
11399
|
+
}
|
|
11400
|
+
|
|
11401
|
+
// dist/managed-runtime/drive-sync-transfer.js
|
|
11402
|
+
import { createHash as createHash2, randomUUID as randomUUID5 } from "node:crypto";
|
|
11403
|
+
import { createReadStream as createReadStream2, createWriteStream } from "node:fs";
|
|
11404
|
+
import { lstat as lstat2, mkdir as mkdir2, rename as rename2, rm, writeFile } from "node:fs/promises";
|
|
11405
|
+
import path17 from "node:path";
|
|
11406
|
+
import { Readable } from "node:stream";
|
|
11407
|
+
import { pipeline } from "node:stream/promises";
|
|
11408
|
+
var DEFAULT_DRIVE_SYNC_TRANSFER_TIMEOUT_MS = DRIVE_TRANSFER_POLICY.networkRequestTimeoutMs;
|
|
11409
|
+
var LocalDriveFileDeferredError = class extends Error {
|
|
11410
|
+
drivePath;
|
|
11411
|
+
constructor(drivePath, message) {
|
|
11412
|
+
super(message ?? `Local drive file changed while it was being read; deferring sync for ${drivePath}`);
|
|
11413
|
+
this.name = "LocalDriveFileDeferredError";
|
|
11414
|
+
this.drivePath = drivePath;
|
|
11415
|
+
}
|
|
11416
|
+
};
|
|
11417
|
+
function isLocalDriveFileDeferredError(error) {
|
|
11418
|
+
return error instanceof LocalDriveFileDeferredError;
|
|
11419
|
+
}
|
|
11420
|
+
async function prepareLocalMutationUploads(params) {
|
|
11421
|
+
const preparedMutations = [];
|
|
11422
|
+
const deferredPaths = [];
|
|
11423
|
+
const requiresUploadApi = params.mutations.some((mutation) => mutation.request.type === "write_file" && mutation.localFilePath && typeof mutation.request.storage_key !== "string");
|
|
11424
|
+
if (requiresUploadApi && !params.api.prepareFileUpload) {
|
|
11425
|
+
throw new Error("Drive sync direct upload API is not configured");
|
|
11426
|
+
}
|
|
11427
|
+
for (const mutation of params.mutations) {
|
|
11428
|
+
const localFilePath = mutation.localFilePath;
|
|
11429
|
+
if (mutation.request.type !== "write_file" || !localFilePath) {
|
|
11430
|
+
preparedMutations.push(mutation);
|
|
11431
|
+
continue;
|
|
11432
|
+
}
|
|
11433
|
+
if (typeof mutation.request.storage_key === "string") {
|
|
11434
|
+
preparedMutations.push(mutation);
|
|
11435
|
+
continue;
|
|
11436
|
+
}
|
|
11437
|
+
const drivePath = readMutationString(mutation.request, "path");
|
|
11438
|
+
try {
|
|
11439
|
+
const contentSha256 = readMutationString(mutation.request, "content_sha256");
|
|
11440
|
+
const sizeBytes = readMutationNumber(mutation.request, "size_bytes");
|
|
11441
|
+
const versionId = buildDeterministicDriveVersionId({
|
|
11442
|
+
driveId: params.driveId,
|
|
11443
|
+
path: drivePath,
|
|
11444
|
+
contentSha256,
|
|
11445
|
+
sizeBytes,
|
|
11446
|
+
baseVersionId: typeof mutation.request.base_version_id === "string" ? mutation.request.base_version_id : null
|
|
11447
|
+
});
|
|
11448
|
+
await assertLocalFileFingerprint(mutation);
|
|
11449
|
+
const prepared = await params.api.prepareFileUpload({
|
|
11450
|
+
sync_client_id: params.syncClientId,
|
|
11451
|
+
drive_id: params.driveId,
|
|
11452
|
+
path: drivePath,
|
|
11453
|
+
version_id: versionId,
|
|
11454
|
+
content_sha256: contentSha256,
|
|
11455
|
+
size_bytes: sizeBytes,
|
|
11456
|
+
mime_type: typeof mutation.request.mime_type === "string" ? mutation.request.mime_type : null
|
|
11457
|
+
});
|
|
11458
|
+
if (prepared.upload_request) {
|
|
11459
|
+
await uploadLocalFileToSignedRequest({
|
|
11460
|
+
filePath: localFilePath,
|
|
11461
|
+
sizeBytes,
|
|
11462
|
+
signedRequest: prepared.upload_request
|
|
11463
|
+
});
|
|
11464
|
+
} else if (prepared.upload_status !== "committed") {
|
|
11465
|
+
throw new Error("Prepared drive upload did not include an upload request");
|
|
11466
|
+
}
|
|
11467
|
+
await assertLocalFileFingerprint(mutation);
|
|
11468
|
+
mutation.request.version_id = prepared.version_id;
|
|
11469
|
+
mutation.request.storage_key = prepared.storage_key;
|
|
11470
|
+
preparedMutations.push(mutation);
|
|
11471
|
+
} catch (error) {
|
|
11472
|
+
if (!isLocalDriveFileDeferredError(error)) {
|
|
11473
|
+
throw error;
|
|
11474
|
+
}
|
|
11475
|
+
deferredPaths.push(error.drivePath);
|
|
11476
|
+
}
|
|
11477
|
+
}
|
|
11478
|
+
return { preparedMutations, deferredPaths };
|
|
11479
|
+
}
|
|
11480
|
+
async function downloadDriveVersionToLocalFile(params) {
|
|
11481
|
+
const response = await params.api.downloadFileVersion({
|
|
11482
|
+
sync_client_id: params.syncClientId,
|
|
11483
|
+
drive_id: params.driveId,
|
|
11484
|
+
version_id: params.versionId,
|
|
11485
|
+
format: "base64",
|
|
11486
|
+
transfer_mode: "direct"
|
|
11487
|
+
});
|
|
11488
|
+
if (response.file.transfer_mode === "direct" && response.file.download_request) {
|
|
11489
|
+
return await downloadSignedRequestToFile({
|
|
11490
|
+
signedRequest: response.file.download_request,
|
|
11491
|
+
targetPath: params.targetPath,
|
|
11492
|
+
expectedSizeBytes: params.expectedSizeBytes,
|
|
11493
|
+
expectedSha256: params.expectedSha256
|
|
11494
|
+
});
|
|
11495
|
+
}
|
|
11496
|
+
if (response.file.content_format !== "base64" || !response.file.content_base64) {
|
|
11497
|
+
throw new Error(`Drive file version ${params.versionId} was not returned as base64`);
|
|
11498
|
+
}
|
|
11499
|
+
const bytes = Buffer.from(response.file.content_base64, "base64");
|
|
11500
|
+
const contentSha256 = sha256Hex3(bytes);
|
|
11501
|
+
if (params.expectedSizeBytes !== null && bytes.byteLength !== params.expectedSizeBytes) {
|
|
11502
|
+
throw new Error(`Downloaded drive file size mismatch for ${params.versionId}`);
|
|
11503
|
+
}
|
|
11504
|
+
if (params.expectedSha256 && contentSha256 !== params.expectedSha256) {
|
|
11505
|
+
throw new Error(`Downloaded drive file hash mismatch for ${params.versionId}`);
|
|
11506
|
+
}
|
|
11507
|
+
await mkdir2(path17.dirname(params.targetPath), { recursive: true });
|
|
11508
|
+
await writeFile(params.targetPath, bytes);
|
|
11509
|
+
return { sizeBytes: bytes.byteLength, contentSha256 };
|
|
11510
|
+
}
|
|
11511
|
+
async function assertLocalFileFingerprint(mutation) {
|
|
11512
|
+
if (!mutation.localFilePath || !mutation.localFingerprint) {
|
|
11513
|
+
return;
|
|
11514
|
+
}
|
|
11515
|
+
const current = await lstat2(mutation.localFilePath);
|
|
11516
|
+
if (!current.isFile() || current.size !== mutation.localFingerprint.sizeBytes || current.mtimeMs !== mutation.localFingerprint.mtimeMs || current.ctimeMs !== mutation.localFingerprint.ctimeMs) {
|
|
11517
|
+
const drivePath = readMutationString(mutation.request, "path");
|
|
11518
|
+
throw new LocalDriveFileDeferredError(drivePath);
|
|
11519
|
+
}
|
|
11520
|
+
}
|
|
11521
|
+
async function uploadLocalFileToSignedRequest(params) {
|
|
11522
|
+
const headers = new Headers(params.signedRequest.headers);
|
|
11523
|
+
headers.set("content-length", String(params.sizeBytes));
|
|
11524
|
+
const response = await fetchWithTimeout(params.signedRequest.url, {
|
|
11525
|
+
method: params.signedRequest.method,
|
|
11526
|
+
headers,
|
|
11527
|
+
body: createReadStream2(params.filePath),
|
|
11528
|
+
duplex: "half"
|
|
11529
|
+
}, {
|
|
11530
|
+
timeoutMs: DEFAULT_DRIVE_SYNC_TRANSFER_TIMEOUT_MS,
|
|
11531
|
+
label: "Drive object upload"
|
|
11532
|
+
});
|
|
11533
|
+
if (!response.ok) {
|
|
11534
|
+
const body = await response.text().catch(() => "");
|
|
11535
|
+
throw new Error(`Drive object upload failed with status ${response.status}${body ? ` body=${body.slice(0, 500)}` : ""}`);
|
|
11536
|
+
}
|
|
11537
|
+
}
|
|
11538
|
+
async function downloadSignedRequestToFile(params) {
|
|
11539
|
+
const response = await fetchWithTimeout(params.signedRequest.url, {
|
|
11540
|
+
method: params.signedRequest.method,
|
|
11541
|
+
headers: params.signedRequest.headers
|
|
11542
|
+
}, {
|
|
11543
|
+
timeoutMs: DEFAULT_DRIVE_SYNC_TRANSFER_TIMEOUT_MS,
|
|
11544
|
+
label: "Drive object download"
|
|
11545
|
+
});
|
|
11546
|
+
if (!response.ok || !response.body) {
|
|
11547
|
+
const body = await response.text().catch(() => "");
|
|
11548
|
+
throw new Error(`Drive object download failed with status ${response.status}${body ? ` body=${body.slice(0, 500)}` : ""}`);
|
|
11549
|
+
}
|
|
11550
|
+
await mkdir2(path17.dirname(params.targetPath), { recursive: true });
|
|
11551
|
+
const tempPath = `${params.targetPath}.panorama-download-${randomUUID5()}.tmp`;
|
|
11552
|
+
const hash = createHash2("sha256");
|
|
11553
|
+
let sizeBytes = 0;
|
|
11554
|
+
try {
|
|
11555
|
+
const source = Readable.fromWeb(response.body);
|
|
11556
|
+
await pipeline(source, async function* hashChunks(chunks) {
|
|
11557
|
+
for await (const chunk of chunks) {
|
|
11558
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
11559
|
+
sizeBytes += buffer.byteLength;
|
|
11560
|
+
hash.update(buffer);
|
|
11561
|
+
yield buffer;
|
|
11562
|
+
}
|
|
11563
|
+
}, createWriteStream(tempPath));
|
|
11564
|
+
const contentSha256 = hash.digest("hex");
|
|
11565
|
+
if (params.expectedSizeBytes !== null && sizeBytes !== params.expectedSizeBytes) {
|
|
11566
|
+
throw new Error(`Downloaded drive file size mismatch for ${params.targetPath}`);
|
|
11567
|
+
}
|
|
11568
|
+
if (params.expectedSha256 && contentSha256 !== params.expectedSha256) {
|
|
11569
|
+
throw new Error(`Downloaded drive file hash mismatch for ${params.targetPath}`);
|
|
11570
|
+
}
|
|
11571
|
+
await rename2(tempPath, params.targetPath);
|
|
11572
|
+
return { sizeBytes, contentSha256 };
|
|
11573
|
+
} catch (error) {
|
|
11574
|
+
await rm(tempPath, { force: true }).catch(() => void 0);
|
|
11575
|
+
throw error;
|
|
11576
|
+
}
|
|
11577
|
+
}
|
|
11578
|
+
async function fetchWithTimeout(input, init, options) {
|
|
11579
|
+
const controller = new AbortController();
|
|
11580
|
+
const timer = setTimeout(() => {
|
|
11581
|
+
controller.abort();
|
|
11582
|
+
}, options.timeoutMs);
|
|
11583
|
+
timer.unref?.();
|
|
11584
|
+
try {
|
|
11585
|
+
return await fetch(input, {
|
|
11586
|
+
...init,
|
|
11587
|
+
signal: init.signal ?? controller.signal
|
|
11588
|
+
});
|
|
11589
|
+
} catch (error) {
|
|
11590
|
+
if (controller.signal.aborted) {
|
|
11591
|
+
throw new Error(`${options.label} timed out after ${options.timeoutMs}ms`);
|
|
11592
|
+
}
|
|
11593
|
+
throw error;
|
|
11594
|
+
} finally {
|
|
11595
|
+
clearTimeout(timer);
|
|
11596
|
+
}
|
|
11597
|
+
}
|
|
11598
|
+
function buildDeterministicDriveVersionId(params) {
|
|
11599
|
+
const hash = createHash2("sha256").update([
|
|
11600
|
+
"panorama-drive-version-v1",
|
|
11601
|
+
params.driveId,
|
|
11602
|
+
normalizeDrivePath(params.path),
|
|
11603
|
+
params.contentSha256,
|
|
11604
|
+
String(params.sizeBytes),
|
|
11605
|
+
params.baseVersionId ?? ""
|
|
11606
|
+
].join("\0")).digest();
|
|
11607
|
+
const bytes = Buffer.from(hash.subarray(0, 16));
|
|
11608
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
11609
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
11610
|
+
const hex = bytes.toString("hex");
|
|
11611
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
11612
|
+
}
|
|
11613
|
+
function readMutationString(request, key) {
|
|
11614
|
+
const value = request[key];
|
|
11615
|
+
if (typeof value !== "string") {
|
|
11616
|
+
throw new Error(`Local drive sync mutation is missing ${key}`);
|
|
11617
|
+
}
|
|
11618
|
+
return value;
|
|
11619
|
+
}
|
|
11620
|
+
function readMutationNumber(request, key) {
|
|
11621
|
+
const value = request[key];
|
|
11622
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
11623
|
+
throw new Error(`Local drive sync mutation is missing numeric ${key}`);
|
|
11624
|
+
}
|
|
11625
|
+
return value;
|
|
11626
|
+
}
|
|
11627
|
+
function sha256Hex3(bytes) {
|
|
11628
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
11629
|
+
}
|
|
11630
|
+
|
|
11631
|
+
// dist/managed-runtime/drive-sync.js
|
|
11632
|
+
var MAX_LOCAL_BATCH_CHANGES = DRIVE_TRANSFER_POLICY.localBatchMaxChanges;
|
|
11633
|
+
var MAX_LOCAL_BATCH_UPLOAD_BYTES = DRIVE_TRANSFER_POLICY.localSyncBatchUploadMaxBytes;
|
|
11634
|
+
var MAX_LOCAL_FILE_UPLOAD_BYTES = DRIVE_TRANSFER_POLICY.objectFileMaxBytes;
|
|
11635
|
+
var DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS2 = DRIVE_TRANSFER_POLICY.networkRequestTimeoutMs;
|
|
11636
|
+
var DRIVE_SYNC_STATUS_CONFLICT_SAMPLE_LIMIT = 10;
|
|
11637
|
+
function isRecord2(value) {
|
|
11638
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
11639
|
+
}
|
|
11640
|
+
function mapDriveSyncEntryRow(row) {
|
|
11641
|
+
return {
|
|
11642
|
+
driveId: row.drive_id,
|
|
11643
|
+
path: row.path,
|
|
11644
|
+
entryType: row.entry_type,
|
|
11645
|
+
versionId: row.version_id,
|
|
11646
|
+
contentSha256: row.content_sha256,
|
|
11647
|
+
sizeBytes: row.size_bytes,
|
|
11648
|
+
mimeType: row.mime_type
|
|
11649
|
+
};
|
|
11650
|
+
}
|
|
11651
|
+
function mapDriveSyncConflictRow(row) {
|
|
11652
|
+
return {
|
|
11653
|
+
driveId: row.drive_id,
|
|
11654
|
+
path: row.path,
|
|
11655
|
+
conflictType: row.conflict_type,
|
|
11656
|
+
reason: row.reason,
|
|
11657
|
+
localContentSha256: row.local_content_sha256,
|
|
11658
|
+
localSizeBytes: row.local_size_bytes,
|
|
11659
|
+
localVersionId: row.local_version_id,
|
|
11660
|
+
conflictId: row.conflict_id,
|
|
11661
|
+
conflictPath: row.conflict_path,
|
|
11662
|
+
remoteBatchId: row.remote_batch_id,
|
|
11663
|
+
remoteSequence: row.remote_sequence
|
|
11664
|
+
};
|
|
11665
|
+
}
|
|
11666
|
+
var DriveSyncStateStore = class _DriveSyncStateStore {
|
|
11667
|
+
db;
|
|
11668
|
+
constructor(db) {
|
|
11669
|
+
this.db = db;
|
|
11670
|
+
}
|
|
11671
|
+
static async open(stateDir) {
|
|
11672
|
+
await mkdir3(stateDir, { recursive: true });
|
|
11673
|
+
const sqlite = await import("node:sqlite");
|
|
11674
|
+
const db = new sqlite.DatabaseSync(path18.join(stateDir, "drive-sync.sqlite"));
|
|
11675
|
+
const store = new _DriveSyncStateStore(db);
|
|
11676
|
+
store.ensureSchema();
|
|
11677
|
+
return store;
|
|
11678
|
+
}
|
|
11679
|
+
close() {
|
|
11680
|
+
this.db.close();
|
|
11681
|
+
}
|
|
11682
|
+
getOrCreateClientKey(prefix) {
|
|
11683
|
+
const key = "client_instance_id";
|
|
11684
|
+
const rows = this.db.prepare("SELECT value FROM drive_sync_metadata WHERE key = ?").all(key);
|
|
11685
|
+
const existing = rows[0]?.value;
|
|
11686
|
+
if (existing) {
|
|
11687
|
+
return `${prefix}:${existing}`;
|
|
11688
|
+
}
|
|
11689
|
+
const clientInstanceId = randomUUID6();
|
|
11690
|
+
this.db.prepare(`
|
|
11691
|
+
INSERT INTO drive_sync_metadata (key, value, updated_at)
|
|
11692
|
+
VALUES (?, ?, datetime('now'))
|
|
11693
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
11694
|
+
value = excluded.value,
|
|
11695
|
+
updated_at = excluded.updated_at
|
|
11696
|
+
`).run(key, clientInstanceId);
|
|
11697
|
+
return `${prefix}:${clientInstanceId}`;
|
|
11698
|
+
}
|
|
11699
|
+
getEntry(driveId, drivePath) {
|
|
11700
|
+
const normalizedPath = normalizeDrivePath(drivePath);
|
|
11701
|
+
const rows = this.db.prepare(`
|
|
11702
|
+
SELECT drive_id, path, entry_type, version_id, content_sha256, size_bytes, mime_type
|
|
11703
|
+
FROM drive_sync_entries
|
|
11704
|
+
WHERE drive_id = ? AND path = ?
|
|
11705
|
+
`).all(driveId, normalizedPath);
|
|
11706
|
+
return rows[0] ? mapDriveSyncEntryRow(rows[0]) : null;
|
|
11707
|
+
}
|
|
11708
|
+
listEntries(driveId) {
|
|
11709
|
+
const rows = this.db.prepare(`
|
|
11710
|
+
SELECT drive_id, path, entry_type, version_id, content_sha256, size_bytes, mime_type
|
|
11711
|
+
FROM drive_sync_entries
|
|
11712
|
+
WHERE drive_id = ?
|
|
11713
|
+
ORDER BY path ASC
|
|
11714
|
+
`).all(driveId);
|
|
11715
|
+
return rows.map(mapDriveSyncEntryRow);
|
|
11716
|
+
}
|
|
11717
|
+
listDrives() {
|
|
11718
|
+
const rows = this.db.prepare(`
|
|
11719
|
+
SELECT drive_id, title, root_path
|
|
11720
|
+
FROM drive_sync_drives
|
|
11721
|
+
ORDER BY drive_id ASC
|
|
11722
|
+
`).all();
|
|
11723
|
+
return rows.map((row) => ({
|
|
11724
|
+
id: row.drive_id,
|
|
11725
|
+
title: row.title,
|
|
11726
|
+
rootPath: row.root_path
|
|
11727
|
+
}));
|
|
11728
|
+
}
|
|
11729
|
+
getConflict(driveId, drivePath) {
|
|
11730
|
+
const normalizedPath = normalizeDrivePath(drivePath);
|
|
11731
|
+
const rows = this.db.prepare(`
|
|
11732
|
+
SELECT
|
|
11733
|
+
drive_id,
|
|
11734
|
+
path,
|
|
11735
|
+
conflict_type,
|
|
11736
|
+
reason,
|
|
11737
|
+
local_content_sha256,
|
|
11738
|
+
local_size_bytes,
|
|
11739
|
+
local_version_id,
|
|
11740
|
+
conflict_id,
|
|
11741
|
+
conflict_path,
|
|
11742
|
+
remote_batch_id,
|
|
11743
|
+
remote_sequence
|
|
11744
|
+
FROM drive_sync_conflicts
|
|
11745
|
+
WHERE drive_id = ? AND path = ?
|
|
11746
|
+
`).all(driveId, normalizedPath);
|
|
11747
|
+
return rows[0] ? mapDriveSyncConflictRow(rows[0]) : null;
|
|
11748
|
+
}
|
|
11749
|
+
listConflicts(driveId) {
|
|
11750
|
+
const rows = this.db.prepare(`
|
|
11751
|
+
SELECT
|
|
11752
|
+
drive_id,
|
|
11753
|
+
path,
|
|
11754
|
+
conflict_type,
|
|
11755
|
+
reason,
|
|
11756
|
+
local_content_sha256,
|
|
11757
|
+
local_size_bytes,
|
|
11758
|
+
local_version_id,
|
|
11759
|
+
conflict_id,
|
|
11760
|
+
conflict_path,
|
|
11761
|
+
remote_batch_id,
|
|
11762
|
+
remote_sequence
|
|
11763
|
+
FROM drive_sync_conflicts
|
|
11764
|
+
WHERE drive_id = ?
|
|
11765
|
+
ORDER BY path ASC
|
|
11766
|
+
`).all(driveId);
|
|
11767
|
+
return rows.map(mapDriveSyncConflictRow);
|
|
11768
|
+
}
|
|
11769
|
+
setSyncClient(client) {
|
|
11770
|
+
this.db.prepare(`
|
|
11771
|
+
INSERT INTO drive_sync_clients (id, team_id, agent_id, host_id, display_name, status, updated_at)
|
|
11772
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
|
11773
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
11774
|
+
team_id = excluded.team_id,
|
|
11775
|
+
agent_id = excluded.agent_id,
|
|
11776
|
+
host_id = excluded.host_id,
|
|
11777
|
+
display_name = excluded.display_name,
|
|
11778
|
+
status = excluded.status,
|
|
11779
|
+
updated_at = excluded.updated_at
|
|
11780
|
+
`).run(client.id, client.team_id, client.agent_id, client.host_id, client.display_name, client.status);
|
|
11781
|
+
}
|
|
11782
|
+
upsertDrive(drive) {
|
|
11783
|
+
this.db.prepare(`
|
|
11784
|
+
INSERT INTO drive_sync_drives (drive_id, title, root_path, updated_at)
|
|
11785
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
11786
|
+
ON CONFLICT(drive_id) DO UPDATE SET
|
|
11787
|
+
title = excluded.title,
|
|
11788
|
+
root_path = excluded.root_path,
|
|
11789
|
+
updated_at = excluded.updated_at
|
|
11790
|
+
`).run(drive.id, drive.title, drive.rootPath);
|
|
11791
|
+
}
|
|
11792
|
+
removeDrive(driveId) {
|
|
11793
|
+
this.db.prepare("DELETE FROM drive_sync_conflicts WHERE drive_id = ?").run(driveId);
|
|
11794
|
+
this.db.prepare("DELETE FROM drive_sync_entries WHERE drive_id = ?").run(driveId);
|
|
11795
|
+
this.db.prepare("DELETE FROM drive_sync_cursors WHERE drive_id = ?").run(driveId);
|
|
11796
|
+
this.db.prepare("DELETE FROM drive_sync_drives WHERE drive_id = ?").run(driveId);
|
|
11797
|
+
}
|
|
11798
|
+
setCursor(driveId, lastBatchSequence) {
|
|
11799
|
+
this.db.prepare(`
|
|
11800
|
+
INSERT INTO drive_sync_cursors (drive_id, last_batch_sequence, updated_at)
|
|
11801
|
+
VALUES (?, ?, datetime('now'))
|
|
11802
|
+
ON CONFLICT(drive_id) DO UPDATE SET
|
|
11803
|
+
last_batch_sequence = excluded.last_batch_sequence,
|
|
11804
|
+
updated_at = excluded.updated_at
|
|
11805
|
+
`).run(driveId, lastBatchSequence);
|
|
11806
|
+
}
|
|
11807
|
+
upsertEntry(entry) {
|
|
11808
|
+
this.db.prepare(`
|
|
11809
|
+
INSERT INTO drive_sync_entries (
|
|
11810
|
+
drive_id,
|
|
11811
|
+
path,
|
|
11812
|
+
entry_type,
|
|
11813
|
+
version_id,
|
|
11814
|
+
content_sha256,
|
|
11815
|
+
size_bytes,
|
|
11816
|
+
mime_type,
|
|
11817
|
+
updated_at
|
|
11818
|
+
)
|
|
11819
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
11820
|
+
ON CONFLICT(drive_id, path) DO UPDATE SET
|
|
11821
|
+
entry_type = excluded.entry_type,
|
|
11822
|
+
version_id = excluded.version_id,
|
|
11823
|
+
content_sha256 = excluded.content_sha256,
|
|
11824
|
+
size_bytes = excluded.size_bytes,
|
|
11825
|
+
mime_type = excluded.mime_type,
|
|
11826
|
+
updated_at = excluded.updated_at
|
|
11827
|
+
`).run(entry.driveId, entry.path, entry.entryType, entry.versionId, entry.contentSha256, entry.sizeBytes, entry.mimeType);
|
|
11828
|
+
}
|
|
11829
|
+
upsertConflict(conflict) {
|
|
11830
|
+
this.db.prepare(`
|
|
11831
|
+
INSERT INTO drive_sync_conflicts (
|
|
11832
|
+
drive_id,
|
|
11833
|
+
path,
|
|
11834
|
+
conflict_type,
|
|
11835
|
+
reason,
|
|
11836
|
+
local_content_sha256,
|
|
11837
|
+
local_size_bytes,
|
|
11838
|
+
local_version_id,
|
|
11839
|
+
conflict_id,
|
|
11840
|
+
conflict_path,
|
|
11841
|
+
remote_batch_id,
|
|
11842
|
+
remote_sequence,
|
|
11843
|
+
updated_at
|
|
11844
|
+
)
|
|
11845
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
11846
|
+
ON CONFLICT(drive_id, path) DO UPDATE SET
|
|
11847
|
+
conflict_type = excluded.conflict_type,
|
|
11848
|
+
reason = excluded.reason,
|
|
11849
|
+
local_content_sha256 = excluded.local_content_sha256,
|
|
11850
|
+
local_size_bytes = excluded.local_size_bytes,
|
|
11851
|
+
local_version_id = excluded.local_version_id,
|
|
11852
|
+
conflict_id = excluded.conflict_id,
|
|
11853
|
+
conflict_path = excluded.conflict_path,
|
|
11854
|
+
remote_batch_id = excluded.remote_batch_id,
|
|
11855
|
+
remote_sequence = excluded.remote_sequence,
|
|
11856
|
+
updated_at = excluded.updated_at
|
|
11857
|
+
`).run(conflict.driveId, conflict.path, conflict.conflictType, conflict.reason, conflict.localContentSha256, conflict.localSizeBytes, conflict.localVersionId, conflict.conflictId, conflict.conflictPath, conflict.remoteBatchId, conflict.remoteSequence);
|
|
11858
|
+
}
|
|
11859
|
+
deleteConflict(driveId, drivePath) {
|
|
11860
|
+
this.db.prepare("DELETE FROM drive_sync_conflicts WHERE drive_id = ? AND path = ?").run(driveId, normalizeDrivePath(drivePath));
|
|
11861
|
+
}
|
|
11862
|
+
deleteConflictTree(driveId, drivePath) {
|
|
11863
|
+
const normalizedPath = normalizeDrivePath(drivePath);
|
|
11864
|
+
if (normalizedPath === "/") {
|
|
11865
|
+
this.db.prepare("DELETE FROM drive_sync_conflicts WHERE drive_id = ?").run(driveId);
|
|
11866
|
+
return;
|
|
11867
|
+
}
|
|
11868
|
+
const prefix = `${normalizedPath}/`;
|
|
11869
|
+
this.db.prepare(`
|
|
11870
|
+
DELETE FROM drive_sync_conflicts
|
|
11871
|
+
WHERE drive_id = ?
|
|
11872
|
+
AND (path = ? OR substr(path, 1, ?) = ?)
|
|
11873
|
+
`).run(driveId, normalizedPath, prefix.length, prefix);
|
|
11874
|
+
}
|
|
11875
|
+
deleteEntryTree(driveId, drivePath) {
|
|
11876
|
+
const normalizedPath = normalizeDrivePath(drivePath);
|
|
11877
|
+
if (normalizedPath === "/") {
|
|
11878
|
+
this.db.prepare("DELETE FROM drive_sync_entries WHERE drive_id = ?").run(driveId);
|
|
11879
|
+
return;
|
|
11880
|
+
}
|
|
11881
|
+
const prefix = `${normalizedPath}/`;
|
|
11882
|
+
this.db.prepare(`
|
|
11883
|
+
DELETE FROM drive_sync_entries
|
|
11884
|
+
WHERE drive_id = ?
|
|
11885
|
+
AND (path = ? OR substr(path, 1, ?) = ?)
|
|
11886
|
+
`).run(driveId, normalizedPath, prefix.length, prefix);
|
|
11887
|
+
}
|
|
11888
|
+
moveEntryTree(driveId, fromPath, toPath) {
|
|
11889
|
+
const normalizedFromPath = normalizeDrivePath(fromPath);
|
|
11890
|
+
const normalizedToPath = normalizeDrivePath(toPath);
|
|
11891
|
+
const prefix = normalizedFromPath === "/" ? "/" : `${normalizedFromPath}/`;
|
|
11892
|
+
const rows = this.db.prepare(`
|
|
11893
|
+
SELECT path, entry_type, version_id, content_sha256, size_bytes, mime_type
|
|
11894
|
+
FROM drive_sync_entries
|
|
11895
|
+
WHERE drive_id = ?
|
|
11896
|
+
AND (path = ? OR substr(path, 1, ?) = ?)
|
|
11897
|
+
`).all(driveId, normalizedFromPath, prefix.length, prefix);
|
|
11898
|
+
this.deleteEntryTree(driveId, normalizedFromPath);
|
|
11899
|
+
for (const row of rows) {
|
|
11900
|
+
const suffix = row.path === normalizedFromPath ? "" : row.path.slice(normalizedFromPath.length);
|
|
11901
|
+
this.upsertEntry({
|
|
11902
|
+
driveId,
|
|
11903
|
+
path: `${normalizedToPath}${suffix}`,
|
|
11904
|
+
entryType: row.entry_type,
|
|
11905
|
+
versionId: row.version_id,
|
|
11906
|
+
contentSha256: row.content_sha256,
|
|
11907
|
+
sizeBytes: row.size_bytes,
|
|
11908
|
+
mimeType: row.mime_type
|
|
11909
|
+
});
|
|
11910
|
+
}
|
|
11911
|
+
}
|
|
11912
|
+
ensureSchema() {
|
|
11913
|
+
this.db.exec(`
|
|
11914
|
+
PRAGMA journal_mode = WAL;
|
|
11915
|
+
CREATE TABLE IF NOT EXISTS drive_sync_metadata (
|
|
11916
|
+
key TEXT PRIMARY KEY,
|
|
11917
|
+
value TEXT NOT NULL,
|
|
11918
|
+
updated_at TEXT NOT NULL
|
|
11919
|
+
);
|
|
11920
|
+
CREATE TABLE IF NOT EXISTS drive_sync_clients (
|
|
11921
|
+
id TEXT PRIMARY KEY,
|
|
11922
|
+
team_id TEXT NOT NULL,
|
|
11923
|
+
agent_id TEXT,
|
|
11924
|
+
host_id TEXT,
|
|
11925
|
+
display_name TEXT,
|
|
11926
|
+
status TEXT NOT NULL,
|
|
11927
|
+
updated_at TEXT NOT NULL
|
|
11928
|
+
);
|
|
11929
|
+
CREATE TABLE IF NOT EXISTS drive_sync_drives (
|
|
11930
|
+
drive_id TEXT PRIMARY KEY,
|
|
11931
|
+
title TEXT,
|
|
11932
|
+
root_path TEXT NOT NULL,
|
|
11933
|
+
updated_at TEXT NOT NULL
|
|
11934
|
+
);
|
|
11935
|
+
CREATE TABLE IF NOT EXISTS drive_sync_cursors (
|
|
11936
|
+
drive_id TEXT PRIMARY KEY,
|
|
11937
|
+
last_batch_sequence INTEGER NOT NULL,
|
|
11938
|
+
updated_at TEXT NOT NULL
|
|
11939
|
+
);
|
|
11940
|
+
CREATE TABLE IF NOT EXISTS drive_sync_entries (
|
|
11941
|
+
drive_id TEXT NOT NULL,
|
|
11942
|
+
path TEXT NOT NULL,
|
|
11943
|
+
entry_type TEXT NOT NULL,
|
|
11944
|
+
version_id TEXT,
|
|
11945
|
+
content_sha256 TEXT,
|
|
11946
|
+
size_bytes INTEGER,
|
|
11947
|
+
mime_type TEXT,
|
|
11948
|
+
updated_at TEXT NOT NULL,
|
|
11949
|
+
PRIMARY KEY (drive_id, path)
|
|
11950
|
+
);
|
|
11951
|
+
CREATE TABLE IF NOT EXISTS drive_sync_conflicts (
|
|
11952
|
+
drive_id TEXT NOT NULL,
|
|
11953
|
+
path TEXT NOT NULL,
|
|
11954
|
+
conflict_type TEXT NOT NULL,
|
|
11955
|
+
reason TEXT,
|
|
11956
|
+
local_content_sha256 TEXT,
|
|
11957
|
+
local_size_bytes INTEGER,
|
|
11958
|
+
local_version_id TEXT,
|
|
11959
|
+
conflict_id TEXT,
|
|
11960
|
+
conflict_path TEXT,
|
|
11961
|
+
remote_batch_id TEXT,
|
|
11962
|
+
remote_sequence INTEGER,
|
|
11963
|
+
updated_at TEXT NOT NULL,
|
|
11964
|
+
PRIMARY KEY (drive_id, path)
|
|
11965
|
+
);
|
|
11966
|
+
`);
|
|
11967
|
+
}
|
|
11968
|
+
};
|
|
11969
|
+
async function createManagedDriveSyncService(params) {
|
|
11970
|
+
const stateStore = params.stateStore ?? await DriveSyncStateStore.open(params.config.driveSync.stateDir);
|
|
11971
|
+
const api = params.api ?? createSupabaseDriveSyncApi(params.supabase, {
|
|
11972
|
+
timeoutMs: params.config.driveSync.networkTimeoutMs
|
|
11973
|
+
});
|
|
11974
|
+
const service = new ManagedDrivePullSyncService({
|
|
11975
|
+
config: params.config,
|
|
11976
|
+
api,
|
|
11977
|
+
stateStore
|
|
11978
|
+
});
|
|
11979
|
+
await mkdir3(params.config.driveSync.rootDir, { recursive: true });
|
|
11980
|
+
return service;
|
|
11981
|
+
}
|
|
11982
|
+
function createSupabaseDriveSyncApi(supabase, options = {}) {
|
|
11983
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_DRIVE_SYNC_NETWORK_TIMEOUT_MS2;
|
|
11984
|
+
return {
|
|
11985
|
+
registerClient: (body) => invokeDriveSyncFunction(supabase, "register-drive-sync-client", body, timeoutMs),
|
|
11986
|
+
listDrives: (body) => invokeDriveSyncFunction(supabase, "list-sync-drives", body, timeoutMs),
|
|
11987
|
+
getChanges: (body) => invokeDriveSyncFunction(supabase, "get-drive-changes", body, timeoutMs),
|
|
11988
|
+
downloadFileVersion: (body) => invokeDriveSyncFunction(supabase, "download-drive-file-version", body, timeoutMs),
|
|
11989
|
+
prepareFileUpload: (body) => invokeDriveSyncFunction(supabase, "prepare-drive-file-upload", body, timeoutMs),
|
|
11990
|
+
ackChanges: (body) => invokeDriveSyncFunction(supabase, "ack-drive-changes", body, timeoutMs),
|
|
11991
|
+
applyLocalChangeBatch: (body) => invokeDriveSyncFunction(supabase, "apply-local-drive-change-batch", body, timeoutMs),
|
|
11992
|
+
applyConflictActions: (body) => invokeDriveSyncFunction(supabase, "apply-drive-conflict-actions", body, timeoutMs),
|
|
11993
|
+
reportStatus: (body) => invokeDriveSyncFunction(supabase, "report-drive-sync-status", body, timeoutMs)
|
|
11994
|
+
};
|
|
11995
|
+
}
|
|
11996
|
+
async function applyDriveChangeBatchToLocalFilesystem(params) {
|
|
11997
|
+
let appliedPathCount = 0;
|
|
11998
|
+
let remoteConflictCount = 0;
|
|
11999
|
+
const remoteConflicts = [];
|
|
12000
|
+
const operations = planRemoteDriveBatch(params.batch);
|
|
12001
|
+
for (const operation of operations) {
|
|
12002
|
+
if (operation.type === "move") {
|
|
12003
|
+
const sourcePath = resolveDriveLocalPath(params.driveRoot, operation.fromPath);
|
|
12004
|
+
const targetPath = resolveDriveLocalPath(params.driveRoot, operation.toPath);
|
|
12005
|
+
await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(sourcePath));
|
|
12006
|
+
const alreadyAppliedMove = await classifyRemoteMoveAlreadyApplied({
|
|
12007
|
+
driveId: params.batch.drive_id,
|
|
12008
|
+
driveRoot: params.driveRoot,
|
|
12009
|
+
stateStore: params.stateStore,
|
|
12010
|
+
fromPath: operation.fromPath,
|
|
12011
|
+
toPath: operation.toPath
|
|
12012
|
+
});
|
|
12013
|
+
if (alreadyAppliedMove) {
|
|
12014
|
+
if (alreadyAppliedMove === "advance_state") {
|
|
12015
|
+
applyRemoteDriveMoveState({
|
|
12016
|
+
stateStore: params.stateStore,
|
|
12017
|
+
driveId: params.batch.drive_id,
|
|
12018
|
+
fromPath: operation.fromPath,
|
|
12019
|
+
toPath: operation.toPath
|
|
12020
|
+
});
|
|
12021
|
+
}
|
|
12022
|
+
appliedPathCount += 1;
|
|
12023
|
+
continue;
|
|
12024
|
+
}
|
|
12025
|
+
const preserved2 = await preserveRemoteApplySteps({
|
|
12026
|
+
driveId: params.batch.drive_id,
|
|
12027
|
+
driveRoot: params.driveRoot,
|
|
12028
|
+
stateStore: params.stateStore,
|
|
12029
|
+
batch: params.batch,
|
|
12030
|
+
steps: operation.preserve
|
|
12031
|
+
});
|
|
12032
|
+
remoteConflictCount += preserved2.conflictCount;
|
|
12033
|
+
remoteConflicts.push(...preserved2.conflicts);
|
|
12034
|
+
await assertPathExists(sourcePath, `Drive move source is missing: ${operation.fromPath}`);
|
|
12035
|
+
await assertLocalPathIsNotSymlink(sourcePath, `Drive move source is a symlink: ${operation.fromPath}`);
|
|
12036
|
+
await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(targetPath));
|
|
12037
|
+
await mkdir3(path18.dirname(targetPath), { recursive: true });
|
|
12038
|
+
await rm2(targetPath, { recursive: true, force: true });
|
|
12039
|
+
await rename3(sourcePath, targetPath);
|
|
12040
|
+
applyRemoteDriveMoveState({
|
|
12041
|
+
stateStore: params.stateStore,
|
|
12042
|
+
driveId: params.batch.drive_id,
|
|
12043
|
+
fromPath: operation.fromPath,
|
|
12044
|
+
toPath: operation.toPath
|
|
12045
|
+
});
|
|
12046
|
+
appliedPathCount += 1;
|
|
12047
|
+
continue;
|
|
12048
|
+
}
|
|
12049
|
+
const localPath = resolveDriveLocalPath(params.driveRoot, operation.path);
|
|
12050
|
+
if (operation.type === "delete") {
|
|
12051
|
+
if (await remoteDeleteAlreadyApplied({
|
|
12052
|
+
driveId: params.batch.drive_id,
|
|
12053
|
+
driveRoot: params.driveRoot,
|
|
12054
|
+
path: operation.path
|
|
12055
|
+
})) {
|
|
12056
|
+
applyRemoteDriveDeleteState({
|
|
12057
|
+
stateStore: params.stateStore,
|
|
12058
|
+
driveId: params.batch.drive_id,
|
|
12059
|
+
path: operation.path
|
|
12060
|
+
});
|
|
12061
|
+
appliedPathCount += 1;
|
|
12062
|
+
continue;
|
|
12063
|
+
}
|
|
12064
|
+
const preserved2 = await preserveRemoteApplySteps({
|
|
12065
|
+
driveId: params.batch.drive_id,
|
|
12066
|
+
driveRoot: params.driveRoot,
|
|
12067
|
+
stateStore: params.stateStore,
|
|
12068
|
+
batch: params.batch,
|
|
12069
|
+
steps: operation.preserve
|
|
12070
|
+
});
|
|
12071
|
+
remoteConflictCount += preserved2.conflictCount;
|
|
12072
|
+
remoteConflicts.push(...preserved2.conflicts);
|
|
12073
|
+
await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(localPath));
|
|
12074
|
+
await rm2(localPath, { recursive: true, force: true });
|
|
12075
|
+
applyRemoteDriveDeleteState({
|
|
12076
|
+
stateStore: params.stateStore,
|
|
12077
|
+
driveId: params.batch.drive_id,
|
|
12078
|
+
path: operation.path
|
|
12079
|
+
});
|
|
12080
|
+
appliedPathCount += 1;
|
|
12081
|
+
continue;
|
|
12082
|
+
}
|
|
12083
|
+
if (operation.type === "mkdir") {
|
|
12084
|
+
await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(localPath));
|
|
12085
|
+
await assertLocalPathIsNotSymlink(localPath, `Drive directory target is a symlink: ${operation.path}`);
|
|
12086
|
+
await mkdir3(localPath, { recursive: true });
|
|
12087
|
+
applyRemoteDriveDirectoryState({
|
|
12088
|
+
stateStore: params.stateStore,
|
|
12089
|
+
driveId: params.batch.drive_id,
|
|
12090
|
+
path: operation.path
|
|
12091
|
+
});
|
|
12092
|
+
appliedPathCount += 1;
|
|
12093
|
+
continue;
|
|
12094
|
+
}
|
|
12095
|
+
const alreadyAppliedFile = await remoteFileAlreadyApplied({
|
|
12096
|
+
driveId: params.batch.drive_id,
|
|
12097
|
+
driveRoot: params.driveRoot,
|
|
12098
|
+
path: operation.path,
|
|
12099
|
+
version: operation.change.version
|
|
12100
|
+
});
|
|
12101
|
+
if (alreadyAppliedFile) {
|
|
12102
|
+
applyRemoteDriveFileState({
|
|
12103
|
+
stateStore: params.stateStore,
|
|
12104
|
+
driveId: params.batch.drive_id,
|
|
12105
|
+
path: operation.path,
|
|
12106
|
+
versionId: operation.versionId,
|
|
12107
|
+
version: operation.change.version,
|
|
12108
|
+
downloadedSizeBytes: alreadyAppliedFile.sizeBytes
|
|
12109
|
+
});
|
|
12110
|
+
appliedPathCount += 1;
|
|
12111
|
+
continue;
|
|
12112
|
+
}
|
|
12113
|
+
const preserved = await preserveRemoteApplySteps({
|
|
12114
|
+
driveId: params.batch.drive_id,
|
|
12115
|
+
driveRoot: params.driveRoot,
|
|
12116
|
+
stateStore: params.stateStore,
|
|
12117
|
+
batch: params.batch,
|
|
12118
|
+
steps: operation.preserve
|
|
12119
|
+
});
|
|
12120
|
+
remoteConflictCount += preserved.conflictCount;
|
|
12121
|
+
remoteConflicts.push(...preserved.conflicts);
|
|
12122
|
+
await assertNoSymlinkInExistingPath(params.driveRoot, path18.dirname(localPath));
|
|
12123
|
+
await assertLocalPathIsNotSymlink(localPath, `Drive file target is a symlink: ${operation.path}`);
|
|
12124
|
+
if (preserved.overwriteResults.get(operation.path)?.localEntry?.entryType === "directory") {
|
|
12125
|
+
await rm2(localPath, { recursive: true, force: true });
|
|
12126
|
+
}
|
|
12127
|
+
const version = operation.change.version;
|
|
12128
|
+
const downloaded = await downloadDriveVersionToLocalFile({
|
|
12129
|
+
api: params.api,
|
|
12130
|
+
syncClientId: params.syncClientId,
|
|
12131
|
+
driveId: params.batch.drive_id,
|
|
12132
|
+
versionId: operation.versionId,
|
|
12133
|
+
targetPath: localPath,
|
|
12134
|
+
expectedSizeBytes: version?.size_bytes ?? null,
|
|
12135
|
+
expectedSha256: version?.content_sha256 ?? null
|
|
12136
|
+
});
|
|
12137
|
+
applyRemoteDriveFileState({
|
|
12138
|
+
stateStore: params.stateStore,
|
|
12139
|
+
driveId: params.batch.drive_id,
|
|
12140
|
+
path: operation.path,
|
|
12141
|
+
versionId: operation.versionId,
|
|
12142
|
+
version,
|
|
12143
|
+
downloadedSizeBytes: downloaded.sizeBytes
|
|
12144
|
+
});
|
|
12145
|
+
appliedPathCount += 1;
|
|
12146
|
+
}
|
|
12147
|
+
return { appliedPathCount, remoteConflictCount, remoteConflicts };
|
|
12148
|
+
}
|
|
12149
|
+
async function classifyRemoteMoveAlreadyApplied(params) {
|
|
12150
|
+
const normalizedFromPath = normalizeDrivePath(params.fromPath);
|
|
12151
|
+
const normalizedToPath = normalizeDrivePath(params.toPath);
|
|
12152
|
+
const localSource = await readLocalDriveEntryAtPath({
|
|
12153
|
+
driveId: params.driveId,
|
|
12154
|
+
driveRoot: params.driveRoot,
|
|
12155
|
+
drivePath: normalizedFromPath
|
|
12156
|
+
});
|
|
12157
|
+
if (localSource) {
|
|
12158
|
+
return null;
|
|
12159
|
+
}
|
|
12160
|
+
const baselineSource = params.stateStore.getEntry?.(params.driveId, normalizedFromPath) ?? null;
|
|
12161
|
+
const baselineTarget = params.stateStore.getEntry?.(params.driveId, normalizedToPath) ?? null;
|
|
12162
|
+
const localTarget = await readLocalDriveEntryAtPath({
|
|
12163
|
+
driveId: params.driveId,
|
|
12164
|
+
driveRoot: params.driveRoot,
|
|
12165
|
+
drivePath: normalizedToPath
|
|
12166
|
+
});
|
|
12167
|
+
if (!localTarget) {
|
|
12168
|
+
return null;
|
|
12169
|
+
}
|
|
12170
|
+
if (baselineTarget && localEntryMatchesBaseline(localTarget, baselineTarget)) {
|
|
12171
|
+
return "already_applied";
|
|
12172
|
+
}
|
|
12173
|
+
if (baselineSource?.entryType === "file" && localEntryMatchesBaseline(localTarget, baselineSource)) {
|
|
12174
|
+
return "advance_state";
|
|
12175
|
+
}
|
|
12176
|
+
return null;
|
|
12177
|
+
}
|
|
12178
|
+
async function remoteDeleteAlreadyApplied(params) {
|
|
12179
|
+
const localEntry = await readLocalDriveEntryAtPath({
|
|
12180
|
+
driveId: params.driveId,
|
|
12181
|
+
driveRoot: params.driveRoot,
|
|
12182
|
+
drivePath: params.path
|
|
12183
|
+
});
|
|
12184
|
+
return localEntry === null;
|
|
12185
|
+
}
|
|
12186
|
+
async function remoteFileAlreadyApplied(params) {
|
|
12187
|
+
if (!params.version) {
|
|
12188
|
+
return null;
|
|
12189
|
+
}
|
|
12190
|
+
const localEntry = await readLocalDriveEntryAtPath({
|
|
12191
|
+
driveId: params.driveId,
|
|
12192
|
+
driveRoot: params.driveRoot,
|
|
12193
|
+
drivePath: params.path
|
|
12194
|
+
});
|
|
12195
|
+
if (localEntry?.entryType !== "file" || localEntry.contentSha256 !== params.version.content_sha256 || localEntry.sizeBytes !== params.version.size_bytes) {
|
|
12196
|
+
return null;
|
|
12197
|
+
}
|
|
12198
|
+
return { sizeBytes: localEntry.sizeBytes ?? params.version.size_bytes };
|
|
12199
|
+
}
|
|
12200
|
+
async function preserveRemoteApplySteps(params) {
|
|
12201
|
+
let conflictCount = 0;
|
|
12202
|
+
const conflicts = [];
|
|
12203
|
+
const overwriteResults = /* @__PURE__ */ new Map();
|
|
12204
|
+
for (const step of params.steps) {
|
|
12205
|
+
if (step.type === "path_conflict_candidate") {
|
|
12206
|
+
const result2 = await preserveRemoteApplyConflictCandidate({
|
|
12207
|
+
driveId: params.driveId,
|
|
12208
|
+
driveRoot: params.driveRoot,
|
|
12209
|
+
drivePath: step.path,
|
|
12210
|
+
stateStore: params.stateStore,
|
|
12211
|
+
batch: params.batch,
|
|
12212
|
+
reason: step.reason
|
|
12213
|
+
});
|
|
12214
|
+
conflictCount += result2.conflictCount;
|
|
12215
|
+
conflicts.push(...result2.conflicts);
|
|
12216
|
+
continue;
|
|
12217
|
+
}
|
|
12218
|
+
const result = await preserveRemoteApplyOverwriteCandidates({
|
|
12219
|
+
driveId: params.driveId,
|
|
12220
|
+
driveRoot: params.driveRoot,
|
|
12221
|
+
drivePath: step.path,
|
|
12222
|
+
stateStore: params.stateStore,
|
|
12223
|
+
batch: params.batch,
|
|
12224
|
+
reason: step.reason,
|
|
12225
|
+
subtreeReason: step.subtreeReason
|
|
12226
|
+
});
|
|
12227
|
+
conflictCount += result.conflictCount;
|
|
12228
|
+
conflicts.push(...result.conflicts);
|
|
12229
|
+
overwriteResults.set(step.path, {
|
|
12230
|
+
localEntry: result.localEntry,
|
|
12231
|
+
baselineEntry: result.baselineEntry
|
|
12232
|
+
});
|
|
12233
|
+
}
|
|
12234
|
+
return { conflictCount, conflicts, overwriteResults };
|
|
12235
|
+
}
|
|
12236
|
+
async function preserveRemoteApplyConflictCandidate(params) {
|
|
12237
|
+
const normalizedPath = normalizeDrivePath(params.drivePath);
|
|
12238
|
+
const baselineEntry = params.stateStore.getEntry?.(params.driveId, normalizedPath) ?? null;
|
|
12239
|
+
const localEntry = await readLocalDriveEntryAtPath({
|
|
12240
|
+
driveId: params.driveId,
|
|
12241
|
+
driveRoot: params.driveRoot,
|
|
12242
|
+
drivePath: normalizedPath
|
|
12243
|
+
});
|
|
12244
|
+
if (!baselineEntry && !localEntry) {
|
|
12245
|
+
return { conflictCount: 0, conflicts: [] };
|
|
12246
|
+
}
|
|
12247
|
+
if (baselineEntry && localEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
|
|
12248
|
+
return { conflictCount: 0, conflicts: [] };
|
|
12249
|
+
}
|
|
12250
|
+
if (!baselineEntry && localEntry?.entryType === "directory") {
|
|
12251
|
+
return { conflictCount: 0, conflicts: [] };
|
|
12252
|
+
}
|
|
12253
|
+
const conflictPath = localEntry?.entryType === "file" ? await writeLocalConflictCopy({
|
|
12254
|
+
driveRoot: params.driveRoot,
|
|
12255
|
+
source: localEntry,
|
|
12256
|
+
sourceRootPath: normalizedPath,
|
|
12257
|
+
conflictRootPath: normalizedPath
|
|
12258
|
+
}) : null;
|
|
12259
|
+
const conflict = recordRemoteApplyConflict({
|
|
12260
|
+
stateStore: params.stateStore,
|
|
12261
|
+
driveId: params.driveId,
|
|
12262
|
+
path: normalizedPath,
|
|
12263
|
+
reason: params.reason,
|
|
12264
|
+
localEntry,
|
|
12265
|
+
baselineEntry,
|
|
12266
|
+
conflictPath,
|
|
12267
|
+
remoteBatchId: params.batch.id,
|
|
12268
|
+
remoteSequence: params.batch.sequence
|
|
12269
|
+
});
|
|
12270
|
+
return { conflictCount: 1, conflicts: [conflict] };
|
|
12271
|
+
}
|
|
12272
|
+
async function preserveRemoteApplyOverwriteCandidates(params) {
|
|
12273
|
+
const normalizedPath = normalizeDrivePath(params.drivePath);
|
|
12274
|
+
const baselineEntry = params.stateStore.getEntry?.(params.driveId, normalizedPath) ?? null;
|
|
12275
|
+
const localEntry = await readLocalDriveEntryAtPath({
|
|
12276
|
+
driveId: params.driveId,
|
|
12277
|
+
driveRoot: params.driveRoot,
|
|
12278
|
+
drivePath: normalizedPath
|
|
12279
|
+
});
|
|
12280
|
+
if (baselineEntry?.entryType === "directory" || localEntry?.entryType === "directory") {
|
|
12281
|
+
const conflictCount = await preserveRemoteApplySubtreeConflictCandidates({
|
|
12282
|
+
driveId: params.driveId,
|
|
12283
|
+
driveRoot: params.driveRoot,
|
|
12284
|
+
drivePath: normalizedPath,
|
|
12285
|
+
stateStore: params.stateStore,
|
|
12286
|
+
batch: params.batch,
|
|
12287
|
+
reason: params.subtreeReason ?? params.reason
|
|
12288
|
+
});
|
|
12289
|
+
return { ...conflictCount, localEntry, baselineEntry };
|
|
12290
|
+
}
|
|
12291
|
+
const conflictResult = await preserveRemoteApplyConflictCandidate({
|
|
12292
|
+
driveId: params.driveId,
|
|
12293
|
+
driveRoot: params.driveRoot,
|
|
12294
|
+
drivePath: normalizedPath,
|
|
12295
|
+
stateStore: params.stateStore,
|
|
12296
|
+
batch: params.batch,
|
|
12297
|
+
reason: params.reason
|
|
12298
|
+
});
|
|
12299
|
+
return { ...conflictResult, localEntry, baselineEntry };
|
|
12300
|
+
}
|
|
12301
|
+
async function preserveRemoteApplySubtreeConflictCandidates(params) {
|
|
12302
|
+
const normalizedPath = normalizeDrivePath(params.drivePath);
|
|
12303
|
+
const baselineEntries = (params.stateStore.listEntries?.(params.driveId) ?? []).filter((entry) => drivePathIsInSubtree(entry.path, normalizedPath));
|
|
12304
|
+
const baselineByPath = new Map(baselineEntries.map((entry) => [entry.path, entry]));
|
|
12305
|
+
const localScan = await scanLocalDriveEntries({
|
|
12306
|
+
driveId: params.driveId,
|
|
12307
|
+
driveRoot: params.driveRoot
|
|
12308
|
+
});
|
|
12309
|
+
for (const deferredPath of localScan.deferredPaths) {
|
|
12310
|
+
if (drivePathIsInSubtree(deferredPath, normalizedPath)) {
|
|
12311
|
+
throw new Error(`Local drive subtree has an unstable file; refusing to delete ${params.drivePath}`);
|
|
12312
|
+
}
|
|
12313
|
+
}
|
|
12314
|
+
let conflictCount = 0;
|
|
12315
|
+
const conflicts = [];
|
|
12316
|
+
for (const [drivePath, localEntry] of localScan.entries) {
|
|
12317
|
+
if (!drivePathIsInSubtree(drivePath, normalizedPath)) {
|
|
12318
|
+
continue;
|
|
12319
|
+
}
|
|
12320
|
+
const baselineEntry = baselineByPath.get(drivePath) ?? null;
|
|
12321
|
+
if (baselineEntry && localEntryMatchesBaseline(localEntry, baselineEntry)) {
|
|
12322
|
+
continue;
|
|
12323
|
+
}
|
|
12324
|
+
if (localEntry.entryType === "directory") {
|
|
12325
|
+
continue;
|
|
12326
|
+
}
|
|
12327
|
+
const conflictPath = localEntry.entryType === "file" ? await writeLocalConflictCopy({
|
|
12328
|
+
driveRoot: params.driveRoot,
|
|
12329
|
+
source: localEntry,
|
|
12330
|
+
sourceRootPath: normalizedPath,
|
|
12331
|
+
conflictRootPath: buildSubtreeConflictRootPath(normalizedPath)
|
|
12332
|
+
}) : null;
|
|
12333
|
+
const conflict = recordRemoteApplyConflict({
|
|
12334
|
+
stateStore: params.stateStore,
|
|
12335
|
+
driveId: params.driveId,
|
|
12336
|
+
path: drivePath,
|
|
12337
|
+
reason: params.reason,
|
|
12338
|
+
localEntry,
|
|
12339
|
+
baselineEntry,
|
|
12340
|
+
conflictPath,
|
|
12341
|
+
remoteBatchId: params.batch.id,
|
|
12342
|
+
remoteSequence: params.batch.sequence
|
|
12343
|
+
});
|
|
12344
|
+
conflicts.push(conflict);
|
|
12345
|
+
conflictCount += 1;
|
|
12346
|
+
}
|
|
12347
|
+
return { conflictCount, conflicts };
|
|
12348
|
+
}
|
|
12349
|
+
async function reconcileLocalDriveToCloud(params) {
|
|
12350
|
+
const fullBaseline = new Map((params.stateStore.listEntries?.(params.driveId) ?? []).map((entry) => [entry.path, entry]));
|
|
12351
|
+
const baseline = params.dirtyDrivePaths ? filterBaselineForDirtyDrivePaths(fullBaseline, params.dirtyDrivePaths) : fullBaseline;
|
|
12352
|
+
const localScan = await scanLocalDriveEntries({
|
|
12353
|
+
driveId: params.driveId,
|
|
12354
|
+
driveRoot: params.driveRoot,
|
|
12355
|
+
rootDrivePaths: params.dirtyDrivePaths ?? void 0
|
|
12356
|
+
});
|
|
12357
|
+
const conflicts = new Map((params.stateStore.listConflicts?.(params.driveId) ?? []).map((conflict) => [
|
|
12358
|
+
conflict.path,
|
|
12359
|
+
conflict
|
|
12360
|
+
]));
|
|
12361
|
+
const scopedConflicts = params.dirtyDrivePaths ? filterConflictsForDirtyDrivePaths(conflicts, params.dirtyDrivePaths) : conflicts;
|
|
12362
|
+
const localPlan = planLocalDriveSync({
|
|
12363
|
+
baseline,
|
|
12364
|
+
local: localScan.entries,
|
|
12365
|
+
deferredPaths: localScan.deferredPaths,
|
|
12366
|
+
conflicts: scopedConflicts
|
|
12367
|
+
});
|
|
12368
|
+
const confirmedConflictActions = await applyCloudDriveConflictLifecycleActions({
|
|
12369
|
+
syncClientId: params.syncClientId,
|
|
12370
|
+
driveId: params.driveId,
|
|
12371
|
+
api: params.api,
|
|
12372
|
+
conflicts: scopedConflicts,
|
|
12373
|
+
actions: localPlan.conflictActions
|
|
12374
|
+
});
|
|
12375
|
+
applyLocalDriveConflictActions({
|
|
12376
|
+
stateStore: params.stateStore,
|
|
12377
|
+
actions: confirmedConflictActions
|
|
12378
|
+
});
|
|
12379
|
+
const mutations = localPlan.mutations;
|
|
12380
|
+
if (mutations.length === 0) {
|
|
12381
|
+
return {
|
|
12382
|
+
...emptyLocalReconcileSummary(),
|
|
12383
|
+
deferredFileCount: localScan.deferredFileCount
|
|
12384
|
+
};
|
|
12385
|
+
}
|
|
12386
|
+
let uploadedBatchCount = 0;
|
|
12387
|
+
let uploadedChangeCount = 0;
|
|
12388
|
+
let uploadedAppliedCount = 0;
|
|
12389
|
+
let uploadedConflictCount = 0;
|
|
12390
|
+
let uploadedUnsupportedCount = 0;
|
|
12391
|
+
let uploadedRejectedCount = 0;
|
|
12392
|
+
let deferredFileCount = localScan.deferredFileCount;
|
|
12393
|
+
const uploadResult = await prepareLocalMutationUploads({
|
|
12394
|
+
syncClientId: params.syncClientId,
|
|
12395
|
+
driveId: params.driveId,
|
|
12396
|
+
api: params.api,
|
|
12397
|
+
mutations
|
|
12398
|
+
});
|
|
12399
|
+
deferredFileCount += uploadResult.deferredPaths.length;
|
|
12400
|
+
const preparedMutations = uploadResult.preparedMutations;
|
|
12401
|
+
if (preparedMutations.length === 0) {
|
|
12402
|
+
return {
|
|
12403
|
+
...emptyLocalReconcileSummary(),
|
|
12404
|
+
deferredFileCount
|
|
12405
|
+
};
|
|
12406
|
+
}
|
|
12407
|
+
const mutationChunks = chunkLocalDriveMutations(preparedMutations);
|
|
12408
|
+
const resourceOperationChunkFingerprints = mutationChunks.map((chunk, chunkIndex) => buildLocalResourceOperationChunkFingerprint({
|
|
12409
|
+
driveId: params.driveId,
|
|
12410
|
+
baseCursorSequence: params.baseCursorSequence,
|
|
12411
|
+
chunkIndex: chunkIndex + 1,
|
|
12412
|
+
chunkCount: mutationChunks.length,
|
|
12413
|
+
mutations: chunk
|
|
12414
|
+
}));
|
|
12415
|
+
const resourceOperationFingerprint = buildLocalResourceOperationFingerprint({
|
|
12416
|
+
driveId: params.driveId,
|
|
12417
|
+
baseCursorSequence: params.baseCursorSequence,
|
|
12418
|
+
chunkFingerprints: resourceOperationChunkFingerprints
|
|
12419
|
+
});
|
|
12420
|
+
const resourceOperationId = `local-reconcile:${resourceOperationFingerprint}`;
|
|
12421
|
+
let finalizedOperationResponse = null;
|
|
12422
|
+
for (const [chunkIndex, chunk] of mutationChunks.entries()) {
|
|
12423
|
+
const response = await params.api.applyLocalChangeBatch({
|
|
12424
|
+
sync_client_id: params.syncClientId,
|
|
12425
|
+
drive_id: params.driveId,
|
|
12426
|
+
resource_operation_id: resourceOperationId,
|
|
12427
|
+
resource_operation_fingerprint: resourceOperationFingerprint,
|
|
12428
|
+
resource_operation_chunk_fingerprint: resourceOperationChunkFingerprints[chunkIndex],
|
|
12429
|
+
resource_operation_chunk_fingerprints: resourceOperationChunkFingerprints,
|
|
12430
|
+
resource_operation_chunk_index: chunkIndex + 1,
|
|
12431
|
+
resource_operation_chunk_count: mutationChunks.length,
|
|
12432
|
+
base_cursor_sequence: params.baseCursorSequence,
|
|
12433
|
+
changes: chunk.map((mutation) => mutation.request)
|
|
12434
|
+
});
|
|
12435
|
+
uploadedBatchCount += 1;
|
|
12436
|
+
uploadedChangeCount += chunk.length;
|
|
12437
|
+
if (isStagedLocalOperationResponse(response)) {
|
|
12438
|
+
continue;
|
|
12439
|
+
}
|
|
12440
|
+
if (isFinalizedFullLocalOperationResponse(response, preparedMutations.length)) {
|
|
12441
|
+
finalizedOperationResponse = response;
|
|
12442
|
+
continue;
|
|
12443
|
+
}
|
|
12444
|
+
await applyAcceptedLocalChangeResults({
|
|
12445
|
+
driveId: params.driveId,
|
|
12446
|
+
driveRoot: params.driveRoot,
|
|
12447
|
+
mutations: chunk,
|
|
12448
|
+
results: response.results,
|
|
12449
|
+
stateStore: params.stateStore
|
|
12450
|
+
});
|
|
12451
|
+
uploadedAppliedCount += response.applied_count;
|
|
12452
|
+
uploadedConflictCount += response.conflict_count;
|
|
12453
|
+
uploadedUnsupportedCount += response.unsupported_count;
|
|
12454
|
+
uploadedRejectedCount += response.rejected_count;
|
|
12455
|
+
}
|
|
12456
|
+
if (finalizedOperationResponse) {
|
|
12457
|
+
await applyAcceptedLocalChangeResults({
|
|
12458
|
+
driveId: params.driveId,
|
|
12459
|
+
driveRoot: params.driveRoot,
|
|
12460
|
+
mutations: preparedMutations,
|
|
12461
|
+
results: finalizedOperationResponse.results,
|
|
12462
|
+
stateStore: params.stateStore
|
|
12463
|
+
});
|
|
12464
|
+
uploadedAppliedCount += finalizedOperationResponse.applied_count;
|
|
12465
|
+
uploadedConflictCount += finalizedOperationResponse.conflict_count;
|
|
12466
|
+
uploadedUnsupportedCount += finalizedOperationResponse.unsupported_count;
|
|
12467
|
+
uploadedRejectedCount += finalizedOperationResponse.rejected_count;
|
|
12468
|
+
}
|
|
12469
|
+
return {
|
|
12470
|
+
uploadedBatchCount,
|
|
12471
|
+
uploadedChangeCount,
|
|
12472
|
+
uploadedAppliedCount,
|
|
12473
|
+
uploadedConflictCount,
|
|
12474
|
+
uploadedUnsupportedCount,
|
|
12475
|
+
uploadedRejectedCount,
|
|
12476
|
+
deferredFileCount
|
|
12477
|
+
};
|
|
12478
|
+
}
|
|
12479
|
+
function emptyLocalReconcileSummary() {
|
|
12480
|
+
return {
|
|
12481
|
+
uploadedBatchCount: 0,
|
|
12482
|
+
uploadedChangeCount: 0,
|
|
12483
|
+
uploadedAppliedCount: 0,
|
|
12484
|
+
uploadedConflictCount: 0,
|
|
12485
|
+
uploadedUnsupportedCount: 0,
|
|
12486
|
+
uploadedRejectedCount: 0,
|
|
12487
|
+
deferredFileCount: 0
|
|
12488
|
+
};
|
|
12489
|
+
}
|
|
12490
|
+
function filterBaselineForDirtyDrivePaths(baseline, dirtyDrivePaths) {
|
|
12491
|
+
if (dirtyDrivePaths.size === 0) {
|
|
12492
|
+
return /* @__PURE__ */ new Map();
|
|
12493
|
+
}
|
|
12494
|
+
const normalizedDirtyPaths = [...dirtyDrivePaths].map(normalizeDrivePath);
|
|
12495
|
+
return new Map([...baseline].filter(([baselinePath]) => normalizedDirtyPaths.some((dirtyPath) => drivePathIsInSubtree(baselinePath, dirtyPath) || drivePathIsInSubtree(dirtyPath, baselinePath))));
|
|
12496
|
+
}
|
|
12497
|
+
function filterConflictsForDirtyDrivePaths(conflicts, dirtyDrivePaths) {
|
|
12498
|
+
if (dirtyDrivePaths.size === 0) {
|
|
12499
|
+
return /* @__PURE__ */ new Map();
|
|
12500
|
+
}
|
|
12501
|
+
const normalizedDirtyPaths = [...dirtyDrivePaths].map(normalizeDrivePath);
|
|
12502
|
+
return new Map([...conflicts].filter(([, conflict]) => normalizedDirtyPaths.some((dirtyPath) => drivePathsIntersect(conflict.path, dirtyPath) || (conflict.conflictPath ? drivePathsIntersect(conflict.conflictPath, dirtyPath) : false))));
|
|
12503
|
+
}
|
|
12504
|
+
function drivePathsIntersect(leftPath, rightPath) {
|
|
12505
|
+
return drivePathIsInSubtree(leftPath, rightPath) || drivePathIsInSubtree(rightPath, leftPath);
|
|
12506
|
+
}
|
|
12507
|
+
async function applyCloudDriveConflictLifecycleActions(params) {
|
|
12508
|
+
if (params.actions.length === 0 || !params.api.applyConflictActions) {
|
|
12509
|
+
return [];
|
|
12510
|
+
}
|
|
12511
|
+
const response = await params.api.applyConflictActions({
|
|
12512
|
+
sync_client_id: params.syncClientId,
|
|
12513
|
+
drive_id: params.driveId,
|
|
12514
|
+
operation_id: buildConflictLifecycleOperationId({
|
|
12515
|
+
driveId: params.driveId,
|
|
12516
|
+
actions: params.actions,
|
|
12517
|
+
conflicts: params.conflicts
|
|
12518
|
+
}),
|
|
12519
|
+
actions: params.actions.map((action) => {
|
|
12520
|
+
const conflict = params.conflicts.get(action.path) ?? null;
|
|
12521
|
+
const payload = {
|
|
12522
|
+
type: action.type,
|
|
12523
|
+
path: action.path,
|
|
12524
|
+
reason: action.reason
|
|
12525
|
+
};
|
|
12526
|
+
if (conflict?.conflictId) {
|
|
12527
|
+
payload.conflict_id = conflict.conflictId;
|
|
12528
|
+
}
|
|
12529
|
+
if (action.type === "update_conflict_path") {
|
|
12530
|
+
payload.conflict_path = action.conflictPath;
|
|
12531
|
+
}
|
|
12532
|
+
return payload;
|
|
12533
|
+
})
|
|
12534
|
+
});
|
|
12535
|
+
return collectConfirmedConflictActions(params.actions, response.results);
|
|
12536
|
+
}
|
|
12537
|
+
function collectConfirmedConflictActions(actions, results) {
|
|
12538
|
+
const confirmedActions = [];
|
|
12539
|
+
for (const result of results) {
|
|
12540
|
+
if (!isConfirmedConflictActionResult(result)) {
|
|
12541
|
+
continue;
|
|
12542
|
+
}
|
|
12543
|
+
const action = actions[result.index];
|
|
12544
|
+
if (!action || result.type !== action.type) {
|
|
12545
|
+
continue;
|
|
12546
|
+
}
|
|
12547
|
+
if (typeof result.path === "string" && normalizeDrivePath(result.path) !== action.path) {
|
|
12548
|
+
continue;
|
|
12549
|
+
}
|
|
12550
|
+
if (action.type === "clear_conflict") {
|
|
12551
|
+
if (result.status === "resolved" || result.status === "ignored") {
|
|
12552
|
+
confirmedActions.push(action);
|
|
12553
|
+
}
|
|
12554
|
+
continue;
|
|
12555
|
+
}
|
|
12556
|
+
if (result.status !== "updated") {
|
|
12557
|
+
continue;
|
|
12558
|
+
}
|
|
12559
|
+
confirmedActions.push({
|
|
12560
|
+
...action,
|
|
12561
|
+
conflictPath: typeof result.conflict_path === "string" ? normalizeDrivePath(result.conflict_path) : action.conflictPath
|
|
12562
|
+
});
|
|
12563
|
+
}
|
|
12564
|
+
return confirmedActions;
|
|
12565
|
+
}
|
|
12566
|
+
function isConfirmedConflictActionResult(result) {
|
|
12567
|
+
return Number.isInteger(result.index) && typeof result.type === "string" && typeof result.status === "string";
|
|
12568
|
+
}
|
|
12569
|
+
function buildConflictLifecycleOperationId(params) {
|
|
12570
|
+
const stablePayload = params.actions.map((action) => {
|
|
12571
|
+
const conflict = params.conflicts.get(action.path) ?? null;
|
|
12572
|
+
return {
|
|
12573
|
+
type: action.type,
|
|
12574
|
+
path: action.path,
|
|
12575
|
+
reason: action.reason,
|
|
12576
|
+
conflict_id: conflict?.conflictId ?? null,
|
|
12577
|
+
conflict_path: action.type === "update_conflict_path" ? action.conflictPath : null
|
|
12578
|
+
};
|
|
12579
|
+
});
|
|
12580
|
+
const digest = sha256Hex4(Buffer.from(JSON.stringify({
|
|
12581
|
+
driveId: params.driveId,
|
|
12582
|
+
actions: stablePayload
|
|
12583
|
+
})));
|
|
12584
|
+
return `conflict-lifecycle:${digest}`;
|
|
12585
|
+
}
|
|
12586
|
+
function collectRemoteConflictsForAck(params) {
|
|
12587
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
12588
|
+
for (const conflict of params.newConflicts) {
|
|
12589
|
+
if (conflict.conflictType === "remote_apply") {
|
|
12590
|
+
byPath.set(conflict.path, conflict);
|
|
12591
|
+
}
|
|
12592
|
+
}
|
|
12593
|
+
for (const conflict of params.stateStore.listConflicts?.(params.driveId) ?? []) {
|
|
12594
|
+
if (conflict.conflictType !== "remote_apply") {
|
|
12595
|
+
continue;
|
|
12596
|
+
}
|
|
12597
|
+
if (conflict.remoteBatchId === params.batch.id || conflict.remoteSequence === params.batch.sequence) {
|
|
12598
|
+
byPath.set(conflict.path, conflict);
|
|
12599
|
+
}
|
|
12600
|
+
}
|
|
12601
|
+
return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
12602
|
+
}
|
|
12603
|
+
function toRemoteConflictAckPayload(conflict) {
|
|
12604
|
+
return {
|
|
12605
|
+
path: conflict.path,
|
|
12606
|
+
reason: conflict.reason,
|
|
12607
|
+
conflict_path: conflict.conflictPath,
|
|
12608
|
+
local_content_sha256: conflict.localContentSha256,
|
|
12609
|
+
local_size_bytes: conflict.localSizeBytes,
|
|
12610
|
+
local_version_id: isUuid2(conflict.localVersionId) ? conflict.localVersionId : null,
|
|
12611
|
+
remote_batch_id: isUuid2(conflict.remoteBatchId) ? conflict.remoteBatchId : null,
|
|
12612
|
+
remote_sequence: conflict.remoteSequence
|
|
12613
|
+
};
|
|
12614
|
+
}
|
|
12615
|
+
function buildRemoteConflictAckOperationId(params) {
|
|
12616
|
+
const stablePayload = params.conflicts.map((conflict) => ({
|
|
12617
|
+
path: conflict.path,
|
|
12618
|
+
reason: conflict.reason,
|
|
12619
|
+
conflict_path: conflict.conflictPath,
|
|
12620
|
+
local_content_sha256: conflict.localContentSha256,
|
|
12621
|
+
local_size_bytes: conflict.localSizeBytes,
|
|
12622
|
+
local_version_id: isUuid2(conflict.localVersionId) ? conflict.localVersionId : null,
|
|
12623
|
+
remote_batch_id: isUuid2(conflict.remoteBatchId) ? conflict.remoteBatchId : null,
|
|
12624
|
+
remote_sequence: conflict.remoteSequence
|
|
12625
|
+
}));
|
|
12626
|
+
const digest = sha256Hex4(Buffer.from(JSON.stringify({
|
|
12627
|
+
driveId: params.driveId,
|
|
12628
|
+
batchId: params.batch.id,
|
|
12629
|
+
batchSequence: params.batch.sequence,
|
|
12630
|
+
conflicts: stablePayload
|
|
12631
|
+
})));
|
|
12632
|
+
return `remote-conflict-ack:${digest}`;
|
|
12633
|
+
}
|
|
12634
|
+
function applyRemoteConflictAckResults(params) {
|
|
12635
|
+
if (!params.results || !params.stateStore.getConflict || !params.stateStore.upsertConflict) {
|
|
12636
|
+
return;
|
|
12637
|
+
}
|
|
12638
|
+
for (const result of params.results) {
|
|
12639
|
+
if (typeof result.path !== "string" || typeof result.conflict_id !== "string") {
|
|
12640
|
+
continue;
|
|
12641
|
+
}
|
|
12642
|
+
const drivePath = normalizeDrivePath(result.path);
|
|
12643
|
+
const existing = params.stateStore.getConflict(params.driveId, drivePath);
|
|
12644
|
+
if (!existing) {
|
|
12645
|
+
continue;
|
|
12646
|
+
}
|
|
12647
|
+
params.stateStore.upsertConflict({
|
|
12648
|
+
...existing,
|
|
12649
|
+
conflictId: result.conflict_id,
|
|
12650
|
+
conflictPath: typeof result.conflict_path === "string" ? result.conflict_path : existing.conflictPath
|
|
12651
|
+
});
|
|
12652
|
+
}
|
|
12653
|
+
}
|
|
12654
|
+
function isUuid2(value) {
|
|
12655
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
12656
|
+
}
|
|
12657
|
+
function isStagedLocalOperationResponse(response) {
|
|
12658
|
+
return response.resource_operation_status === "applying" && response.batch === null;
|
|
12659
|
+
}
|
|
12660
|
+
function isFinalizedFullLocalOperationResponse(response, preparedMutationCount) {
|
|
12661
|
+
return response.resource_operation_status === "finalized" && response.results.length >= preparedMutationCount;
|
|
12662
|
+
}
|
|
12663
|
+
function chunkLocalDriveMutations(mutations) {
|
|
12664
|
+
const chunks = [];
|
|
12665
|
+
let currentChunk = [];
|
|
12666
|
+
let currentUploadBytes = 0;
|
|
12667
|
+
for (const mutation of mutations) {
|
|
12668
|
+
if (mutation.uploadBytes > MAX_LOCAL_BATCH_UPLOAD_BYTES) {
|
|
12669
|
+
throw new Error(getDriveLocalSyncFileTooLargeMessage(mutation.uploadBytes));
|
|
12670
|
+
}
|
|
12671
|
+
const nextUploadBytes = currentUploadBytes + mutation.uploadBytes;
|
|
12672
|
+
if (currentChunk.length > 0 && (currentChunk.length >= MAX_LOCAL_BATCH_CHANGES || nextUploadBytes > MAX_LOCAL_BATCH_UPLOAD_BYTES)) {
|
|
12673
|
+
chunks.push(currentChunk);
|
|
12674
|
+
currentChunk = [];
|
|
12675
|
+
currentUploadBytes = 0;
|
|
12676
|
+
}
|
|
12677
|
+
currentChunk.push(mutation);
|
|
12678
|
+
currentUploadBytes += mutation.uploadBytes;
|
|
12679
|
+
}
|
|
12680
|
+
if (currentChunk.length > 0) {
|
|
12681
|
+
chunks.push(currentChunk);
|
|
12682
|
+
}
|
|
12683
|
+
return chunks;
|
|
12684
|
+
}
|
|
12685
|
+
function buildLocalResourceOperationChunkFingerprint(params) {
|
|
12686
|
+
return sha256Hex4(Buffer.from(stableJsonStringify({
|
|
12687
|
+
drive_id: params.driveId,
|
|
12688
|
+
base_cursor_sequence: params.baseCursorSequence,
|
|
12689
|
+
chunk_index: params.chunkIndex,
|
|
12690
|
+
chunk_count: params.chunkCount,
|
|
12691
|
+
changes: params.mutations.map((mutation) => mutation.request)
|
|
12692
|
+
})));
|
|
12693
|
+
}
|
|
12694
|
+
function buildLocalResourceOperationFingerprint(params) {
|
|
12695
|
+
return sha256Hex4(Buffer.from(stableJsonStringify({
|
|
12696
|
+
drive_id: params.driveId,
|
|
12697
|
+
base_cursor_sequence: params.baseCursorSequence,
|
|
12698
|
+
chunk_fingerprints: params.chunkFingerprints
|
|
12699
|
+
})));
|
|
12700
|
+
}
|
|
12701
|
+
async function applyAcceptedLocalChangeResults(params) {
|
|
12702
|
+
for (const result of params.results) {
|
|
12703
|
+
const mutation = params.mutations[result.index];
|
|
12704
|
+
if (!mutation) {
|
|
12705
|
+
continue;
|
|
12706
|
+
}
|
|
12707
|
+
if (result.status === "applied") {
|
|
12708
|
+
applyAppliedLocalDriveMutationState({
|
|
12709
|
+
stateStore: params.stateStore,
|
|
12710
|
+
driveId: params.driveId,
|
|
12711
|
+
mutation,
|
|
12712
|
+
result
|
|
12713
|
+
});
|
|
12714
|
+
continue;
|
|
12715
|
+
}
|
|
12716
|
+
if (result.status === "conflicted") {
|
|
12717
|
+
const conflictPath = resolveLocalMutationResultPath(result, mutation);
|
|
12718
|
+
const localConflictPath = await preserveLocalUploadConflictCandidate({
|
|
12719
|
+
driveRoot: params.driveRoot,
|
|
12720
|
+
mutation,
|
|
12721
|
+
conflictPath
|
|
12722
|
+
});
|
|
12723
|
+
recordLocalUploadConflict({
|
|
12724
|
+
stateStore: params.stateStore,
|
|
12725
|
+
driveId: params.driveId,
|
|
12726
|
+
mutation,
|
|
12727
|
+
result,
|
|
12728
|
+
conflictPath,
|
|
12729
|
+
localConflictPath
|
|
12730
|
+
});
|
|
12731
|
+
continue;
|
|
12732
|
+
}
|
|
12733
|
+
if (result.status === "unsupported" && mutation.stateEntry) {
|
|
12734
|
+
applyUnsupportedLocalDriveMutationState({
|
|
12735
|
+
stateStore: params.stateStore,
|
|
12736
|
+
driveId: params.driveId,
|
|
12737
|
+
mutation
|
|
12738
|
+
});
|
|
12739
|
+
}
|
|
12740
|
+
}
|
|
12741
|
+
}
|
|
12742
|
+
async function preserveLocalUploadConflictCandidate(params) {
|
|
12743
|
+
if (params.mutation.request.type !== "write_file" || !params.mutation.localFilePath || params.mutation.stateEntry?.entryType !== "file") {
|
|
12744
|
+
return null;
|
|
12745
|
+
}
|
|
12746
|
+
try {
|
|
12747
|
+
return await writeLocalConflictCopy({
|
|
12748
|
+
driveRoot: params.driveRoot,
|
|
12749
|
+
source: {
|
|
12750
|
+
driveId: params.mutation.stateEntry.driveId,
|
|
12751
|
+
path: params.mutation.stateEntry.path,
|
|
12752
|
+
entryType: "file",
|
|
12753
|
+
contentSha256: params.mutation.stateEntry.contentSha256,
|
|
12754
|
+
sizeBytes: params.mutation.stateEntry.sizeBytes,
|
|
12755
|
+
mimeType: params.mutation.stateEntry.mimeType,
|
|
12756
|
+
absolutePath: params.mutation.localFilePath
|
|
12757
|
+
},
|
|
12758
|
+
sourceRootPath: params.conflictPath,
|
|
12759
|
+
conflictRootPath: params.conflictPath
|
|
12760
|
+
});
|
|
12761
|
+
} catch (error) {
|
|
12762
|
+
console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} failed to preserve local upload conflict copy`, {
|
|
12763
|
+
path: params.conflictPath,
|
|
12764
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12765
|
+
});
|
|
12766
|
+
return null;
|
|
12767
|
+
}
|
|
12768
|
+
}
|
|
12769
|
+
function emptyDriveSyncRunSummary(driveCount = 0) {
|
|
12770
|
+
return {
|
|
12771
|
+
driveCount,
|
|
12772
|
+
failedDriveCount: 0,
|
|
12773
|
+
appliedBatchCount: 0,
|
|
12774
|
+
appliedPathCount: 0,
|
|
12775
|
+
remoteConflictCount: 0,
|
|
12776
|
+
uploadedBatchCount: 0,
|
|
12777
|
+
uploadedChangeCount: 0,
|
|
12778
|
+
uploadedAppliedCount: 0,
|
|
12779
|
+
uploadedConflictCount: 0,
|
|
12780
|
+
uploadedUnsupportedCount: 0,
|
|
12781
|
+
uploadedRejectedCount: 0,
|
|
12782
|
+
deferredFileCount: 0
|
|
12783
|
+
};
|
|
12784
|
+
}
|
|
12785
|
+
function toDriveSyncStatusSummary(summary) {
|
|
12786
|
+
return {
|
|
12787
|
+
applied_batch_count: summary.appliedBatchCount,
|
|
12788
|
+
applied_path_count: summary.appliedPathCount,
|
|
12789
|
+
remote_conflict_count: summary.remoteConflictCount,
|
|
12790
|
+
uploaded_batch_count: summary.uploadedBatchCount,
|
|
12791
|
+
uploaded_change_count: summary.uploadedChangeCount,
|
|
12792
|
+
uploaded_applied_count: summary.uploadedAppliedCount,
|
|
12793
|
+
uploaded_conflict_count: summary.uploadedConflictCount,
|
|
12794
|
+
uploaded_unsupported_count: summary.uploadedUnsupportedCount,
|
|
12795
|
+
uploaded_rejected_count: summary.uploadedRejectedCount,
|
|
12796
|
+
deferred_file_count: summary.deferredFileCount
|
|
12797
|
+
};
|
|
12798
|
+
}
|
|
12799
|
+
function truncateDriveSyncStatusError(error) {
|
|
12800
|
+
if (!error || error.length <= 2e3) {
|
|
12801
|
+
return error;
|
|
12802
|
+
}
|
|
12803
|
+
return `${error.slice(0, 1997)}...`;
|
|
12804
|
+
}
|
|
12805
|
+
function deriveDriveSyncStatus(schedulerStatus) {
|
|
12806
|
+
if (!schedulerStatus) {
|
|
12807
|
+
return "active";
|
|
12808
|
+
}
|
|
12809
|
+
return schedulerStatus.watchStatus === "active" ? "active" : "degraded";
|
|
12810
|
+
}
|
|
12811
|
+
function driveSummaryRequiresDegradedStatus(summary) {
|
|
12812
|
+
return summary.remoteConflictCount > 0 || summary.uploadedConflictCount > 0 || summary.uploadedUnsupportedCount > 0 || summary.uploadedRejectedCount > 0 || summary.deferredFileCount > 0 || summary.failedDriveCount > 0;
|
|
12813
|
+
}
|
|
12814
|
+
function summarizeDriveSyncConflicts(conflicts) {
|
|
12815
|
+
return {
|
|
12816
|
+
unresolvedConflictCount: conflicts.length,
|
|
12817
|
+
unresolvedConflictSamples: conflicts.slice(0, DRIVE_SYNC_STATUS_CONFLICT_SAMPLE_LIMIT).map((conflict) => ({
|
|
12818
|
+
path: conflict.path,
|
|
12819
|
+
conflict_type: conflict.conflictType,
|
|
12820
|
+
reason: conflict.reason,
|
|
12821
|
+
conflict_path: conflict.conflictPath,
|
|
12822
|
+
conflict_id: conflict.conflictId,
|
|
12823
|
+
remote_sequence: conflict.remoteSequence
|
|
12824
|
+
}))
|
|
12825
|
+
};
|
|
12826
|
+
}
|
|
12827
|
+
var ManagedDrivePullSyncService = class {
|
|
12828
|
+
params;
|
|
12829
|
+
constructor(params) {
|
|
12830
|
+
this.params = params;
|
|
12831
|
+
}
|
|
12832
|
+
close() {
|
|
12833
|
+
this.params.stateStore.close?.();
|
|
12834
|
+
}
|
|
12835
|
+
async syncOnce(options = {}) {
|
|
12836
|
+
const includeLocalWrites = options.includeLocalWrites !== false;
|
|
12837
|
+
const syncStatus = deriveDriveSyncStatus(options.schedulerStatus);
|
|
12838
|
+
const syncClient = await this.ensureSyncClient();
|
|
12839
|
+
const drivesResponse = await this.params.api.listDrives({ sync_client_id: syncClient.id });
|
|
12840
|
+
this.params.stateStore.setSyncClient(drivesResponse.sync_client);
|
|
12841
|
+
await this.parkStaleDrives(new Set(drivesResponse.drives.map((drive) => drive.resource.id)));
|
|
12842
|
+
let appliedBatchCount = 0;
|
|
12843
|
+
let appliedPathCount = 0;
|
|
12844
|
+
let remoteConflictCount = 0;
|
|
12845
|
+
let uploadedBatchCount = 0;
|
|
12846
|
+
let uploadedChangeCount = 0;
|
|
12847
|
+
let uploadedAppliedCount = 0;
|
|
12848
|
+
let uploadedConflictCount = 0;
|
|
12849
|
+
let uploadedUnsupportedCount = 0;
|
|
12850
|
+
let uploadedRejectedCount = 0;
|
|
12851
|
+
let deferredFileCount = 0;
|
|
12852
|
+
let failedDriveCount = 0;
|
|
12853
|
+
for (const drive of drivesResponse.drives) {
|
|
12854
|
+
const driveRoot = resolveDriveLocalRoot(this.params.config.driveSync.rootDir, drive.resource.id);
|
|
12855
|
+
const dirtyDrivePaths = resolveDirtyDrivePathsForDrive(driveRoot, options.dirtyLocalPaths);
|
|
12856
|
+
const driveSummary = emptyDriveSyncRunSummary(1);
|
|
12857
|
+
let lastBatchSequence = drive.cursor?.last_batch_sequence ?? 0;
|
|
12858
|
+
try {
|
|
12859
|
+
await mkdir3(driveRoot, { recursive: true });
|
|
12860
|
+
this.params.stateStore.upsertDrive({
|
|
12861
|
+
id: drive.resource.id,
|
|
12862
|
+
title: drive.resource.title,
|
|
12863
|
+
rootPath: driveRoot
|
|
12864
|
+
});
|
|
12865
|
+
const result = await this.syncDrive({
|
|
12866
|
+
syncClientId: syncClient.id,
|
|
12867
|
+
driveId: drive.resource.id,
|
|
12868
|
+
driveRoot
|
|
12869
|
+
});
|
|
12870
|
+
lastBatchSequence = result.lastBatchSequence;
|
|
12871
|
+
driveSummary.appliedBatchCount += result.appliedBatchCount;
|
|
12872
|
+
driveSummary.appliedPathCount += result.appliedPathCount;
|
|
12873
|
+
driveSummary.remoteConflictCount += result.remoteConflictCount;
|
|
12874
|
+
if (includeLocalWrites && (!dirtyDrivePaths || dirtyDrivePaths.size > 0)) {
|
|
12875
|
+
const localResult = await reconcileLocalDriveToCloud({
|
|
12876
|
+
syncClientId: syncClient.id,
|
|
12877
|
+
driveId: drive.resource.id,
|
|
12878
|
+
driveRoot,
|
|
12879
|
+
baseCursorSequence: result.lastBatchSequence,
|
|
12880
|
+
api: this.params.api,
|
|
12881
|
+
stateStore: this.params.stateStore,
|
|
12882
|
+
dirtyDrivePaths
|
|
12883
|
+
});
|
|
12884
|
+
driveSummary.uploadedBatchCount += localResult.uploadedBatchCount;
|
|
12885
|
+
driveSummary.uploadedChangeCount += localResult.uploadedChangeCount;
|
|
12886
|
+
driveSummary.uploadedAppliedCount += localResult.uploadedAppliedCount;
|
|
12887
|
+
driveSummary.uploadedConflictCount += localResult.uploadedConflictCount;
|
|
12888
|
+
driveSummary.uploadedUnsupportedCount += localResult.uploadedUnsupportedCount;
|
|
12889
|
+
driveSummary.uploadedRejectedCount += localResult.uploadedRejectedCount;
|
|
12890
|
+
driveSummary.deferredFileCount += localResult.deferredFileCount;
|
|
12891
|
+
if (localResult.uploadedBatchCount > 0) {
|
|
12892
|
+
const followUpResult = await this.syncDrive({
|
|
12893
|
+
syncClientId: syncClient.id,
|
|
12894
|
+
driveId: drive.resource.id,
|
|
12895
|
+
driveRoot
|
|
12896
|
+
});
|
|
12897
|
+
lastBatchSequence = followUpResult.lastBatchSequence;
|
|
12898
|
+
driveSummary.appliedBatchCount += followUpResult.appliedBatchCount;
|
|
12899
|
+
driveSummary.appliedPathCount += followUpResult.appliedPathCount;
|
|
12900
|
+
driveSummary.remoteConflictCount += followUpResult.remoteConflictCount;
|
|
12901
|
+
}
|
|
12902
|
+
}
|
|
12903
|
+
appliedBatchCount += driveSummary.appliedBatchCount;
|
|
12904
|
+
appliedPathCount += driveSummary.appliedPathCount;
|
|
12905
|
+
remoteConflictCount += driveSummary.remoteConflictCount;
|
|
12906
|
+
uploadedBatchCount += driveSummary.uploadedBatchCount;
|
|
12907
|
+
uploadedChangeCount += driveSummary.uploadedChangeCount;
|
|
12908
|
+
uploadedAppliedCount += driveSummary.uploadedAppliedCount;
|
|
12909
|
+
uploadedConflictCount += driveSummary.uploadedConflictCount;
|
|
12910
|
+
uploadedUnsupportedCount += driveSummary.uploadedUnsupportedCount;
|
|
12911
|
+
uploadedRejectedCount += driveSummary.uploadedRejectedCount;
|
|
12912
|
+
deferredFileCount += driveSummary.deferredFileCount;
|
|
12913
|
+
await this.reportDriveSyncStatus({
|
|
12914
|
+
syncClientId: syncClient.id,
|
|
12915
|
+
driveId: drive.resource.id,
|
|
12916
|
+
driveRoot,
|
|
12917
|
+
status: syncStatus,
|
|
12918
|
+
lastBatchSequence,
|
|
12919
|
+
summary: driveSummary,
|
|
12920
|
+
lastReason: includeLocalWrites ? "sync_once" : "sync_once_remote_only",
|
|
12921
|
+
lastError: null,
|
|
12922
|
+
schedulerStatus: options.schedulerStatus
|
|
12923
|
+
});
|
|
12924
|
+
} catch (error) {
|
|
12925
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
12926
|
+
failedDriveCount += 1;
|
|
12927
|
+
await this.reportDriveSyncStatus({
|
|
12928
|
+
syncClientId: syncClient.id,
|
|
12929
|
+
driveId: drive.resource.id,
|
|
12930
|
+
driveRoot,
|
|
12931
|
+
status: "error",
|
|
12932
|
+
lastBatchSequence,
|
|
12933
|
+
summary: driveSummary,
|
|
12934
|
+
lastReason: "sync_error",
|
|
12935
|
+
lastError: errorMessage,
|
|
12936
|
+
schedulerStatus: options.schedulerStatus
|
|
12937
|
+
});
|
|
12938
|
+
console.error(`${MANAGED_GATEWAY_LOG_PREFIX} failed to sync drive`, {
|
|
12939
|
+
driveId: drive.resource.id,
|
|
12940
|
+
error: errorMessage
|
|
12941
|
+
});
|
|
12942
|
+
}
|
|
12943
|
+
}
|
|
12944
|
+
return {
|
|
12945
|
+
driveCount: drivesResponse.drives.length,
|
|
12946
|
+
failedDriveCount,
|
|
12947
|
+
appliedBatchCount,
|
|
12948
|
+
appliedPathCount,
|
|
12949
|
+
remoteConflictCount,
|
|
12950
|
+
uploadedBatchCount,
|
|
12951
|
+
uploadedChangeCount,
|
|
12952
|
+
uploadedAppliedCount,
|
|
12953
|
+
uploadedConflictCount,
|
|
12954
|
+
uploadedUnsupportedCount,
|
|
12955
|
+
uploadedRejectedCount,
|
|
12956
|
+
deferredFileCount
|
|
12957
|
+
};
|
|
12958
|
+
}
|
|
12959
|
+
async reportDriveSyncStatus(params) {
|
|
12960
|
+
try {
|
|
12961
|
+
const unresolvedConflictSummary = summarizeDriveSyncConflicts(this.params.stateStore.listConflicts?.(params.driveId) ?? []);
|
|
12962
|
+
const status = params.status === "active" && (unresolvedConflictSummary.unresolvedConflictCount > 0 || driveSummaryRequiresDegradedStatus(params.summary)) ? "degraded" : params.status;
|
|
12963
|
+
await this.params.api.reportStatus({
|
|
12964
|
+
sync_client_id: params.syncClientId,
|
|
12965
|
+
drive_id: params.driveId,
|
|
12966
|
+
status,
|
|
12967
|
+
last_reason: params.lastReason,
|
|
12968
|
+
last_error: truncateDriveSyncStatusError(params.lastError),
|
|
12969
|
+
last_batch_sequence: params.lastBatchSequence,
|
|
12970
|
+
summary: toDriveSyncStatusSummary(params.summary),
|
|
12971
|
+
metadata: {
|
|
12972
|
+
root_path: params.driveRoot,
|
|
12973
|
+
runtime_id: this.params.config.runtimeId,
|
|
12974
|
+
host_id: this.params.config.hostId,
|
|
12975
|
+
agent_id: this.params.config.agentId,
|
|
12976
|
+
watcher_status: params.schedulerStatus?.watchStatus ?? null,
|
|
12977
|
+
watcher_degraded: params.schedulerStatus ? params.schedulerStatus.watchStatus !== "active" : null,
|
|
12978
|
+
watcher_error: truncateDriveSyncStatusError(params.schedulerStatus?.watchError ?? null),
|
|
12979
|
+
unresolved_conflict_count: unresolvedConflictSummary.unresolvedConflictCount,
|
|
12980
|
+
unresolved_conflicts: unresolvedConflictSummary.unresolvedConflictSamples
|
|
12981
|
+
}
|
|
12982
|
+
});
|
|
12983
|
+
} catch (error) {
|
|
12984
|
+
console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} failed to report drive sync status`, {
|
|
12985
|
+
driveId: params.driveId,
|
|
12986
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12987
|
+
});
|
|
12988
|
+
}
|
|
12989
|
+
}
|
|
12990
|
+
async parkStaleDrives(accessibleDriveIds) {
|
|
12991
|
+
const knownDrives = this.params.stateStore.listDrives?.() ?? [];
|
|
12992
|
+
for (const knownDrive of knownDrives) {
|
|
12993
|
+
if (accessibleDriveIds.has(knownDrive.id)) {
|
|
12994
|
+
continue;
|
|
12995
|
+
}
|
|
12996
|
+
const parkedPath = await parkStaleDriveLocalFolder(this.params.config.driveSync.rootDir, knownDrive.id);
|
|
12997
|
+
this.params.stateStore.removeDrive?.(knownDrive.id);
|
|
12998
|
+
console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} parked stale drive folder`, {
|
|
12999
|
+
driveId: knownDrive.id,
|
|
13000
|
+
parkedPath
|
|
13001
|
+
});
|
|
13002
|
+
}
|
|
13003
|
+
}
|
|
13004
|
+
async ensureSyncClient() {
|
|
13005
|
+
const clientKey = this.params.config.driveSync.clientKey ?? this.params.stateStore.getOrCreateClientKey?.(`managed:${this.params.config.hostId}:${this.params.config.agentId}`) ?? `managed:${this.params.config.hostId}:${this.params.config.agentId}`;
|
|
13006
|
+
const response = await this.params.api.registerClient({
|
|
13007
|
+
agent_id: this.params.config.agentId,
|
|
13008
|
+
host_id: this.params.config.hostId,
|
|
13009
|
+
client_key: clientKey,
|
|
13010
|
+
display_name: `${hostname2()} managed drive sync`,
|
|
13011
|
+
metadata: {
|
|
13012
|
+
mode: "managed_gateway_pull",
|
|
13013
|
+
runtime_id: this.params.config.runtimeId,
|
|
13014
|
+
process_id: process17.pid
|
|
13015
|
+
}
|
|
13016
|
+
});
|
|
13017
|
+
this.params.stateStore.setSyncClient(response.sync_client);
|
|
13018
|
+
return response.sync_client;
|
|
13019
|
+
}
|
|
13020
|
+
async syncDrive(params) {
|
|
13021
|
+
let appliedBatchCount = 0;
|
|
13022
|
+
let appliedPathCount = 0;
|
|
13023
|
+
let remoteConflictCount = 0;
|
|
13024
|
+
let lastAckedSequence = 0;
|
|
13025
|
+
while (appliedBatchCount < this.params.config.driveSync.maxBatchesPerTick) {
|
|
13026
|
+
const remainingBatchCapacity = this.params.config.driveSync.maxBatchesPerTick - appliedBatchCount;
|
|
13027
|
+
const response = await this.params.api.getChanges({
|
|
13028
|
+
sync_client_id: params.syncClientId,
|
|
13029
|
+
drive_id: params.driveId,
|
|
13030
|
+
limit: Math.min(this.params.config.driveSync.changeLimit, remainingBatchCapacity)
|
|
13031
|
+
});
|
|
13032
|
+
lastAckedSequence = response.cursor.last_batch_sequence;
|
|
13033
|
+
if (response.changes.length === 0) {
|
|
13034
|
+
this.params.stateStore.setCursor(params.driveId, lastAckedSequence);
|
|
13035
|
+
break;
|
|
13036
|
+
}
|
|
13037
|
+
for (const batch of response.changes) {
|
|
13038
|
+
try {
|
|
13039
|
+
const result = await applyDriveChangeBatchToLocalFilesystem({
|
|
13040
|
+
syncClientId: params.syncClientId,
|
|
13041
|
+
driveRoot: params.driveRoot,
|
|
13042
|
+
batch,
|
|
13043
|
+
api: this.params.api,
|
|
13044
|
+
stateStore: this.params.stateStore
|
|
13045
|
+
});
|
|
13046
|
+
const remoteConflicts = collectRemoteConflictsForAck({
|
|
13047
|
+
driveId: params.driveId,
|
|
13048
|
+
batch,
|
|
13049
|
+
newConflicts: result.remoteConflicts,
|
|
13050
|
+
stateStore: this.params.stateStore
|
|
13051
|
+
});
|
|
13052
|
+
const ackResponse = await this.params.api.ackChanges({
|
|
13053
|
+
sync_client_id: params.syncClientId,
|
|
13054
|
+
drive_id: params.driveId,
|
|
13055
|
+
operation_id: remoteConflicts.length > 0 ? buildRemoteConflictAckOperationId({
|
|
13056
|
+
driveId: params.driveId,
|
|
13057
|
+
batch,
|
|
13058
|
+
conflicts: remoteConflicts
|
|
13059
|
+
}) : void 0,
|
|
13060
|
+
last_batch_sequence: batch.sequence,
|
|
13061
|
+
last_error: null,
|
|
13062
|
+
remote_conflicts: remoteConflicts.length > 0 ? remoteConflicts.map(toRemoteConflictAckPayload) : void 0
|
|
13063
|
+
});
|
|
13064
|
+
applyRemoteConflictAckResults({
|
|
13065
|
+
driveId: params.driveId,
|
|
13066
|
+
stateStore: this.params.stateStore,
|
|
13067
|
+
results: ackResponse.remote_conflicts
|
|
13068
|
+
});
|
|
13069
|
+
lastAckedSequence = ackResponse.cursor.last_batch_sequence;
|
|
13070
|
+
this.params.stateStore.setCursor(params.driveId, lastAckedSequence);
|
|
13071
|
+
appliedBatchCount += 1;
|
|
13072
|
+
appliedPathCount += result.appliedPathCount;
|
|
13073
|
+
remoteConflictCount += result.remoteConflictCount;
|
|
13074
|
+
} catch (error) {
|
|
13075
|
+
await this.params.api.ackChanges({
|
|
13076
|
+
sync_client_id: params.syncClientId,
|
|
13077
|
+
drive_id: params.driveId,
|
|
13078
|
+
last_batch_sequence: lastAckedSequence,
|
|
13079
|
+
last_error: error instanceof Error ? error.message : String(error)
|
|
13080
|
+
});
|
|
13081
|
+
throw error;
|
|
13082
|
+
}
|
|
13083
|
+
}
|
|
13084
|
+
if (!response.has_more) {
|
|
13085
|
+
break;
|
|
13086
|
+
}
|
|
13087
|
+
}
|
|
13088
|
+
return {
|
|
13089
|
+
appliedBatchCount,
|
|
13090
|
+
appliedPathCount,
|
|
13091
|
+
remoteConflictCount,
|
|
13092
|
+
lastBatchSequence: lastAckedSequence
|
|
13093
|
+
};
|
|
13094
|
+
}
|
|
13095
|
+
};
|
|
13096
|
+
async function invokeDriveSyncFunction(supabase, functionName, body, timeoutMs) {
|
|
13097
|
+
const { data, error } = await withTimeout(supabase.functions.invoke(functionName, { body }), timeoutMs, `${functionName} invocation`);
|
|
13098
|
+
if (error) {
|
|
13099
|
+
throw new Error(`${functionName} failed: ${await formatSupabaseFunctionError(error)}`);
|
|
13100
|
+
}
|
|
13101
|
+
if (!data?.success) {
|
|
13102
|
+
throw new Error(`${functionName} returned an invalid response`);
|
|
13103
|
+
}
|
|
13104
|
+
return data;
|
|
13105
|
+
}
|
|
13106
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
13107
|
+
let timer = null;
|
|
13108
|
+
const timeout = new Promise((_, reject) => {
|
|
13109
|
+
timer = setTimeout(() => {
|
|
13110
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
13111
|
+
}, timeoutMs);
|
|
13112
|
+
timer.unref?.();
|
|
13113
|
+
});
|
|
13114
|
+
try {
|
|
13115
|
+
return await Promise.race([promise, timeout]);
|
|
13116
|
+
} finally {
|
|
13117
|
+
if (timer) {
|
|
13118
|
+
clearTimeout(timer);
|
|
13119
|
+
}
|
|
13120
|
+
}
|
|
13121
|
+
}
|
|
13122
|
+
async function formatSupabaseFunctionError(error) {
|
|
13123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13124
|
+
const context = isRecord2(error) ? error.context : null;
|
|
13125
|
+
if (!(context instanceof Response)) {
|
|
13126
|
+
return message;
|
|
13127
|
+
}
|
|
13128
|
+
const responseBody = await context.text().catch(() => null);
|
|
13129
|
+
const bodySuffix = responseBody && responseBody.trim().length > 0 ? ` body=${responseBody.trim().slice(0, 1e3)}` : "";
|
|
13130
|
+
return `${message} (status=${context.status}${bodySuffix})`;
|
|
13131
|
+
}
|
|
13132
|
+
function sha256Hex4(bytes) {
|
|
13133
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
13134
|
+
}
|
|
13135
|
+
function stableJsonStringify(input) {
|
|
13136
|
+
if (input === void 0) {
|
|
13137
|
+
return "null";
|
|
13138
|
+
}
|
|
13139
|
+
if (input === null || typeof input !== "object") {
|
|
13140
|
+
return JSON.stringify(input);
|
|
13141
|
+
}
|
|
13142
|
+
if (Array.isArray(input)) {
|
|
13143
|
+
return `[${input.map((value) => stableJsonStringify(value)).join(",")}]`;
|
|
13144
|
+
}
|
|
13145
|
+
const record = input;
|
|
13146
|
+
return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableJsonStringify(record[key])}`).join(",")}}`;
|
|
13147
|
+
}
|
|
13148
|
+
function logDriveSyncRunSummary(summary) {
|
|
13149
|
+
if (summary.driveCount === 0 || summary.appliedBatchCount === 0 && summary.uploadedBatchCount === 0 && summary.remoteConflictCount === 0 && summary.deferredFileCount === 0 && summary.failedDriveCount === 0) {
|
|
13150
|
+
return;
|
|
13151
|
+
}
|
|
13152
|
+
console.log(`${MANAGED_GATEWAY_LOG_PREFIX} applied drive sync changes`, {
|
|
13153
|
+
driveCount: summary.driveCount,
|
|
13154
|
+
failedDriveCount: summary.failedDriveCount,
|
|
13155
|
+
appliedBatchCount: summary.appliedBatchCount,
|
|
13156
|
+
appliedPathCount: summary.appliedPathCount,
|
|
13157
|
+
remoteConflictCount: summary.remoteConflictCount,
|
|
13158
|
+
uploadedBatchCount: summary.uploadedBatchCount,
|
|
13159
|
+
uploadedChangeCount: summary.uploadedChangeCount,
|
|
13160
|
+
uploadedAppliedCount: summary.uploadedAppliedCount,
|
|
13161
|
+
uploadedConflictCount: summary.uploadedConflictCount,
|
|
13162
|
+
uploadedUnsupportedCount: summary.uploadedUnsupportedCount,
|
|
13163
|
+
uploadedRejectedCount: summary.uploadedRejectedCount,
|
|
13164
|
+
deferredFileCount: summary.deferredFileCount
|
|
13165
|
+
});
|
|
13166
|
+
}
|
|
13167
|
+
|
|
13168
|
+
// ../shared/dist/control-signals.js
|
|
13169
|
+
var CONTROL_ACTIONS = /* @__PURE__ */ new Set(["continue", "pause", "stop"]);
|
|
13170
|
+
function parseControlSignalCandidate(value) {
|
|
13171
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
13172
|
+
return null;
|
|
13173
|
+
}
|
|
13174
|
+
const rawNextAction = value.nextAction;
|
|
13175
|
+
if (typeof rawNextAction !== "string" || !CONTROL_ACTIONS.has(rawNextAction)) {
|
|
13176
|
+
return null;
|
|
13177
|
+
}
|
|
13178
|
+
const pauseDurationValue = value.pauseDuration ?? value.seconds;
|
|
13179
|
+
const pauseDuration = typeof pauseDurationValue === "number" && Number.isFinite(pauseDurationValue) && pauseDurationValue > 0 ? pauseDurationValue : void 0;
|
|
13180
|
+
return {
|
|
13181
|
+
nextAction: rawNextAction,
|
|
13182
|
+
pauseDuration
|
|
13183
|
+
};
|
|
13184
|
+
}
|
|
13185
|
+
|
|
13186
|
+
// dist/managed-runtime/shell-execution.js
|
|
13187
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
13188
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
13189
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
13190
|
+
import { tmpdir } from "node:os";
|
|
13191
|
+
import { join } from "node:path";
|
|
13192
|
+
var DEFAULT_SIGTERM_GRACE_MS = 5e3;
|
|
13193
|
+
var DEFAULT_OUTPUT_CAPTURE_BYTES2 = 5e6;
|
|
13194
|
+
var CONTROL_SIGNAL_PATH_ENV = "PANORAMA_CONTROL_SIGNAL_PATH";
|
|
13195
|
+
async function executeCommand(command, timeoutMs, options = {}) {
|
|
13196
|
+
const startedAt = Date.now();
|
|
13197
|
+
const outputCaptureBytes = options.outputCaptureBytes ?? DEFAULT_OUTPUT_CAPTURE_BYTES2;
|
|
13198
|
+
return await new Promise((resolve) => {
|
|
13199
|
+
const child = spawn4("bash", ["-lc", command], {
|
|
13200
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13201
|
+
detached: true,
|
|
13202
|
+
env: options.env
|
|
13203
|
+
});
|
|
13204
|
+
const stdoutOutput = new BoundedOutputCapture(outputCaptureBytes);
|
|
13205
|
+
const stderrOutput = new BoundedOutputCapture(outputCaptureBytes);
|
|
13206
|
+
let resolved = false;
|
|
13207
|
+
let timedOut = false;
|
|
13208
|
+
let spawnError = null;
|
|
13209
|
+
let interrupted = false;
|
|
13210
|
+
let forceKillHandle = null;
|
|
13211
|
+
const finish = (result) => {
|
|
13212
|
+
if (resolved)
|
|
13213
|
+
return;
|
|
13214
|
+
resolved = true;
|
|
13215
|
+
clearTimeout(timeoutHandle);
|
|
13216
|
+
if (forceKillHandle) {
|
|
13217
|
+
clearTimeout(forceKillHandle);
|
|
13218
|
+
forceKillHandle = null;
|
|
13219
|
+
}
|
|
13220
|
+
options.registerCancellation?.(null);
|
|
13221
|
+
resolve(result);
|
|
13222
|
+
};
|
|
13223
|
+
const killProcessGroup = (signal) => {
|
|
13224
|
+
if (!child.pid)
|
|
13225
|
+
return;
|
|
13226
|
+
try {
|
|
13227
|
+
process.kill(-child.pid, signal);
|
|
13228
|
+
} catch {
|
|
13229
|
+
try {
|
|
13230
|
+
child.kill(signal);
|
|
13231
|
+
} catch {
|
|
13232
|
+
}
|
|
13233
|
+
}
|
|
13234
|
+
};
|
|
13235
|
+
const scheduleForceKill = () => {
|
|
13236
|
+
if (forceKillHandle)
|
|
13237
|
+
return;
|
|
13238
|
+
forceKillHandle = setTimeout(() => {
|
|
13239
|
+
forceKillHandle = null;
|
|
13240
|
+
killProcessGroup("SIGKILL");
|
|
13241
|
+
}, DEFAULT_SIGTERM_GRACE_MS);
|
|
13242
|
+
forceKillHandle.unref();
|
|
13243
|
+
};
|
|
13244
|
+
const timeoutHandle = setTimeout(() => {
|
|
13245
|
+
if (resolved)
|
|
13246
|
+
return;
|
|
13247
|
+
timedOut = true;
|
|
13248
|
+
killProcessGroup("SIGTERM");
|
|
13249
|
+
scheduleForceKill();
|
|
13250
|
+
}, timeoutMs);
|
|
13251
|
+
options.registerCancellation?.(() => {
|
|
13252
|
+
if (resolved || interrupted)
|
|
13253
|
+
return;
|
|
13254
|
+
interrupted = true;
|
|
13255
|
+
killProcessGroup("SIGTERM");
|
|
13256
|
+
scheduleForceKill();
|
|
13257
|
+
});
|
|
13258
|
+
child.stdout?.on("data", (chunk) => {
|
|
13259
|
+
stdoutOutput.append(chunk);
|
|
13260
|
+
});
|
|
13261
|
+
child.stderr?.on("data", (chunk) => {
|
|
13262
|
+
stderrOutput.append(chunk);
|
|
13263
|
+
});
|
|
13264
|
+
child.on("error", (error) => {
|
|
13265
|
+
spawnError = error.message;
|
|
13266
|
+
});
|
|
13267
|
+
child.on("close", (code) => {
|
|
13268
|
+
const stdout = stdoutOutput.readText();
|
|
13269
|
+
const stderr = stderrOutput.readText();
|
|
13270
|
+
const durationMs = Date.now() - startedAt;
|
|
13271
|
+
const commonResult = {
|
|
13272
|
+
stdout,
|
|
13273
|
+
stderr,
|
|
13274
|
+
stdoutObservedBytes: stdoutOutput.observedBytes,
|
|
13275
|
+
stderrObservedBytes: stderrOutput.observedBytes,
|
|
13276
|
+
stdoutCapturedBytes: stdoutOutput.capturedBytes,
|
|
13277
|
+
stderrCapturedBytes: stderrOutput.capturedBytes,
|
|
13278
|
+
stdoutTruncated: stdoutOutput.truncated,
|
|
13279
|
+
stderrTruncated: stderrOutput.truncated,
|
|
13280
|
+
outputCaptureBytes,
|
|
13281
|
+
exitCode: code,
|
|
13282
|
+
durationMs
|
|
13283
|
+
};
|
|
13284
|
+
if (timedOut) {
|
|
13285
|
+
finish({
|
|
13286
|
+
...commonResult,
|
|
13287
|
+
status: "timed_out",
|
|
13288
|
+
error: `Command timed out after ${timeoutMs}ms`
|
|
13289
|
+
});
|
|
13290
|
+
return;
|
|
13291
|
+
}
|
|
13292
|
+
if (interrupted) {
|
|
13293
|
+
finish({
|
|
13294
|
+
...commonResult,
|
|
13295
|
+
status: "cancelled",
|
|
13296
|
+
error: "Command was interrupted because the managed gateway host was stopping."
|
|
13297
|
+
});
|
|
13298
|
+
return;
|
|
13299
|
+
}
|
|
13300
|
+
if (spawnError) {
|
|
13301
|
+
finish({
|
|
13302
|
+
...commonResult,
|
|
13303
|
+
status: "failed",
|
|
13304
|
+
error: spawnError
|
|
13305
|
+
});
|
|
13306
|
+
return;
|
|
13307
|
+
}
|
|
13308
|
+
finish({
|
|
13309
|
+
...commonResult,
|
|
13310
|
+
status: code === 0 ? "succeeded" : "failed",
|
|
13311
|
+
error: code === 0 ? null : `Command exited with code ${code ?? "unknown"}`
|
|
13312
|
+
});
|
|
13313
|
+
});
|
|
13314
|
+
});
|
|
13315
|
+
}
|
|
13316
|
+
function buildManagedShellExecutionEnv(params) {
|
|
13317
|
+
const safeEnv = {};
|
|
13318
|
+
const passthroughKeys = [
|
|
13319
|
+
"HOME",
|
|
13320
|
+
"HOSTNAME",
|
|
13321
|
+
"LANG",
|
|
13322
|
+
"LC_ALL",
|
|
13323
|
+
"LOGNAME",
|
|
13324
|
+
"PATH",
|
|
13325
|
+
"PWD",
|
|
13326
|
+
"SHELL",
|
|
13327
|
+
"SHLVL",
|
|
13328
|
+
"TERM",
|
|
13329
|
+
"TMPDIR",
|
|
13330
|
+
"USER"
|
|
13331
|
+
];
|
|
13332
|
+
for (const key of passthroughKeys) {
|
|
13333
|
+
const value = params.baseEnv[key];
|
|
13334
|
+
if (typeof value === "string" && value.length > 0) {
|
|
13335
|
+
safeEnv[key] = value;
|
|
13336
|
+
}
|
|
13337
|
+
}
|
|
13338
|
+
safeEnv.PANORAMA_AGENT_ID = params.agentId;
|
|
13339
|
+
safeEnv.PANORAMA_TOOL_EXECUTION_URL = params.toolExecutionUrl;
|
|
13340
|
+
safeEnv.PANORAMA_TOOL_EXECUTION_TOKEN = params.toolExecutionToken;
|
|
13341
|
+
safeEnv[CONTROL_SIGNAL_PATH_ENV] = params.controlSignalPath;
|
|
13342
|
+
if (params.cycleId) {
|
|
13343
|
+
safeEnv.PANORAMA_AGENT_CYCLE_ID = params.cycleId;
|
|
13344
|
+
}
|
|
13345
|
+
return safeEnv;
|
|
13346
|
+
}
|
|
13347
|
+
function buildControlSignalPath(executionId) {
|
|
13348
|
+
return join(tmpdir(), `panorama-control-${executionId}-${randomUUID7()}.json`);
|
|
13349
|
+
}
|
|
13350
|
+
async function readCommandControlSignal(path21) {
|
|
13351
|
+
try {
|
|
13352
|
+
const raw = await readFile(path21, "utf8");
|
|
13353
|
+
return parseControlSignalCandidate(JSON.parse(raw));
|
|
13354
|
+
} catch {
|
|
13355
|
+
return null;
|
|
13356
|
+
} finally {
|
|
13357
|
+
try {
|
|
13358
|
+
await unlink(path21);
|
|
13359
|
+
} catch {
|
|
13360
|
+
}
|
|
13361
|
+
}
|
|
13362
|
+
}
|
|
13363
|
+
|
|
13364
|
+
// dist/managed-runtime/dependencies.js
|
|
13365
|
+
var defaultManagedGatewayRuntimeDependencies = {
|
|
13366
|
+
heartbeatLinuxHost,
|
|
13367
|
+
dispatchLinuxExecution,
|
|
13368
|
+
startLinuxExecution,
|
|
10557
13369
|
completeLinuxExecution,
|
|
10558
13370
|
reconcileExecutionLifecycle,
|
|
10559
13371
|
executeCommand,
|
|
10560
13372
|
createRealtimeClient: createManagedRealtimeClient,
|
|
10561
13373
|
startRealtimeWakeLoop: startManagedRealtimeWakeLoop,
|
|
13374
|
+
createDriveSyncService: createManagedDriveSyncService,
|
|
10562
13375
|
registerSignalHandlers(onSignal) {
|
|
10563
13376
|
process.once("SIGINT", onSignal);
|
|
10564
13377
|
process.once("SIGTERM", onSignal);
|
|
@@ -10576,7 +13389,7 @@ function resolveManagedGatewayRuntimeDependencies(dependencies = {}) {
|
|
|
10576
13389
|
}
|
|
10577
13390
|
|
|
10578
13391
|
// dist/managed-runtime/execution-dispatcher.js
|
|
10579
|
-
import
|
|
13392
|
+
import process18 from "node:process";
|
|
10580
13393
|
|
|
10581
13394
|
// ../shared/dist/cycle-step-artifacts.js
|
|
10582
13395
|
var RESERVED_TOOL_RESULT_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -10967,7 +13780,7 @@ async function executeManagedLinuxExecution(params) {
|
|
|
10967
13780
|
try {
|
|
10968
13781
|
result = await dependencies.executeCommand(command, config.execTimeoutMs, {
|
|
10969
13782
|
env: buildManagedShellExecutionEnv({
|
|
10970
|
-
baseEnv:
|
|
13783
|
+
baseEnv: process18.env,
|
|
10971
13784
|
agentId: config.agentId,
|
|
10972
13785
|
cycleId: startedExecution.cycle_id,
|
|
10973
13786
|
toolExecutionUrl: config.toolExecutionUrl,
|
|
@@ -11055,6 +13868,295 @@ async function executeManagedLinuxExecution(params) {
|
|
|
11055
13868
|
return { reconciliationRequired: false };
|
|
11056
13869
|
}
|
|
11057
13870
|
|
|
13871
|
+
// dist/managed-runtime/drive-sync-scheduler.js
|
|
13872
|
+
import { lstatSync, readdirSync, watch } from "node:fs";
|
|
13873
|
+
import path19 from "node:path";
|
|
13874
|
+
function startManagedDriveSyncScheduler(params) {
|
|
13875
|
+
let stopped = false;
|
|
13876
|
+
let running = false;
|
|
13877
|
+
let pendingReason = null;
|
|
13878
|
+
let pendingRequiresLocalWrites = false;
|
|
13879
|
+
let pendingRequiresFullLocalScan = false;
|
|
13880
|
+
let pendingDirtyLocalPaths = /* @__PURE__ */ new Set();
|
|
13881
|
+
let pendingWaiters = [];
|
|
13882
|
+
let debounceTimer = null;
|
|
13883
|
+
let pollTimer = null;
|
|
13884
|
+
let currentRun = null;
|
|
13885
|
+
let watchHandle = null;
|
|
13886
|
+
let watchStatus = "unavailable";
|
|
13887
|
+
let lastReason = null;
|
|
13888
|
+
let lastStartedAt = null;
|
|
13889
|
+
let lastFinishedAt = null;
|
|
13890
|
+
let lastError = null;
|
|
13891
|
+
let watchError = null;
|
|
13892
|
+
let lastSummary = null;
|
|
13893
|
+
let lastSuccessfulLocalWriteSyncAt = Date.now();
|
|
13894
|
+
let hasCompletedLocalWriteSync = false;
|
|
13895
|
+
const localAuditIntervalMs = params.localAuditIntervalMs ?? 3e5;
|
|
13896
|
+
const completeWaiters = (waiters, result) => {
|
|
13897
|
+
for (const waiter of waiters) {
|
|
13898
|
+
waiter.resolve(result);
|
|
13899
|
+
}
|
|
13900
|
+
};
|
|
13901
|
+
const clearDebounceTimer = () => {
|
|
13902
|
+
if (!debounceTimer) {
|
|
13903
|
+
return;
|
|
13904
|
+
}
|
|
13905
|
+
clearTimeout(debounceTimer);
|
|
13906
|
+
debounceTimer = null;
|
|
13907
|
+
};
|
|
13908
|
+
const runPendingSync = async () => {
|
|
13909
|
+
if (stopped || running || !pendingReason) {
|
|
13910
|
+
return;
|
|
13911
|
+
}
|
|
13912
|
+
clearDebounceTimer();
|
|
13913
|
+
const reason = pendingReason;
|
|
13914
|
+
const includeLocalWrites = pendingRequiresLocalWrites;
|
|
13915
|
+
const dirtyLocalPaths = pendingRequiresFullLocalScan ? void 0 : [...pendingDirtyLocalPaths];
|
|
13916
|
+
const waiters = pendingWaiters;
|
|
13917
|
+
pendingReason = null;
|
|
13918
|
+
pendingRequiresLocalWrites = false;
|
|
13919
|
+
pendingRequiresFullLocalScan = false;
|
|
13920
|
+
pendingDirtyLocalPaths = /* @__PURE__ */ new Set();
|
|
13921
|
+
pendingWaiters = [];
|
|
13922
|
+
running = true;
|
|
13923
|
+
lastReason = reason;
|
|
13924
|
+
lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13925
|
+
lastError = null;
|
|
13926
|
+
const run2 = (async () => {
|
|
13927
|
+
let result = {
|
|
13928
|
+
status: "failed",
|
|
13929
|
+
error: "Drive sync did not produce a result"
|
|
13930
|
+
};
|
|
13931
|
+
try {
|
|
13932
|
+
const syncOptions = {
|
|
13933
|
+
includeLocalWrites,
|
|
13934
|
+
dirtyLocalPaths,
|
|
13935
|
+
schedulerStatus: {
|
|
13936
|
+
watchStatus,
|
|
13937
|
+
watchError
|
|
13938
|
+
}
|
|
13939
|
+
};
|
|
13940
|
+
const summary = await params.service.syncOnce(syncOptions);
|
|
13941
|
+
lastSummary = summary;
|
|
13942
|
+
logDriveSyncRunSummary(summary);
|
|
13943
|
+
result = summary.failedDriveCount > 0 ? {
|
|
13944
|
+
status: "degraded",
|
|
13945
|
+
summary,
|
|
13946
|
+
error: `${summary.failedDriveCount} drive sync${summary.failedDriveCount === 1 ? "" : "s"} failed`
|
|
13947
|
+
} : { status: "success", summary };
|
|
13948
|
+
} catch (error) {
|
|
13949
|
+
const normalizedError = asError(error);
|
|
13950
|
+
lastError = normalizedError.message;
|
|
13951
|
+
result = { status: "failed", error: normalizedError.message };
|
|
13952
|
+
console.error(`${MANAGED_GATEWAY_LOG_PREFIX} failed to sync drives`, {
|
|
13953
|
+
reason,
|
|
13954
|
+
error: normalizedError
|
|
13955
|
+
});
|
|
13956
|
+
} finally {
|
|
13957
|
+
if (includeLocalWrites && (result.status === "success" || result.status === "degraded" && result.summary.failedDriveCount === 0)) {
|
|
13958
|
+
lastSuccessfulLocalWriteSyncAt = Date.now();
|
|
13959
|
+
hasCompletedLocalWriteSync = true;
|
|
13960
|
+
}
|
|
13961
|
+
lastFinishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13962
|
+
running = false;
|
|
13963
|
+
completeWaiters(waiters, result);
|
|
13964
|
+
if (pendingReason && !stopped) {
|
|
13965
|
+
schedulePendingSync(false);
|
|
13966
|
+
}
|
|
13967
|
+
}
|
|
13968
|
+
})();
|
|
13969
|
+
currentRun = run2;
|
|
13970
|
+
await run2;
|
|
13971
|
+
if (currentRun === run2) {
|
|
13972
|
+
currentRun = null;
|
|
13973
|
+
}
|
|
13974
|
+
};
|
|
13975
|
+
const schedulePendingSync = (immediate) => {
|
|
13976
|
+
if (stopped || running || !pendingReason) {
|
|
13977
|
+
return;
|
|
13978
|
+
}
|
|
13979
|
+
if (immediate) {
|
|
13980
|
+
clearDebounceTimer();
|
|
13981
|
+
void runPendingSync();
|
|
13982
|
+
return;
|
|
13983
|
+
}
|
|
13984
|
+
if (debounceTimer) {
|
|
13985
|
+
return;
|
|
13986
|
+
}
|
|
13987
|
+
debounceTimer = setTimeout(() => {
|
|
13988
|
+
debounceTimer = null;
|
|
13989
|
+
void runPendingSync();
|
|
13990
|
+
}, params.debounceMs);
|
|
13991
|
+
debounceTimer.unref?.();
|
|
13992
|
+
};
|
|
13993
|
+
const requestSync = (reason, options = {}) => {
|
|
13994
|
+
if (stopped) {
|
|
13995
|
+
return Promise.resolve({ status: "skipped", reason: "scheduler_stopped" });
|
|
13996
|
+
}
|
|
13997
|
+
pendingReason = reason;
|
|
13998
|
+
const requiresLocalWrites = requestRequiresLocalWrites({
|
|
13999
|
+
reason,
|
|
14000
|
+
watchStatus,
|
|
14001
|
+
lastSuccessfulLocalWriteSyncAt,
|
|
14002
|
+
hasCompletedLocalWriteSync,
|
|
14003
|
+
localAuditIntervalMs
|
|
14004
|
+
});
|
|
14005
|
+
pendingRequiresLocalWrites = pendingRequiresLocalWrites || requiresLocalWrites;
|
|
14006
|
+
if (options.dirtyLocalPaths && !pendingRequiresFullLocalScan) {
|
|
14007
|
+
for (const dirtyPath of options.dirtyLocalPaths) {
|
|
14008
|
+
pendingDirtyLocalPaths.add(dirtyPath);
|
|
14009
|
+
}
|
|
14010
|
+
} else if (requiresLocalWrites) {
|
|
14011
|
+
pendingRequiresFullLocalScan = true;
|
|
14012
|
+
pendingDirtyLocalPaths.clear();
|
|
14013
|
+
}
|
|
14014
|
+
const promise = new Promise((resolve) => {
|
|
14015
|
+
pendingWaiters.push({ resolve });
|
|
14016
|
+
});
|
|
14017
|
+
schedulePendingSync(options.immediate === true);
|
|
14018
|
+
return promise;
|
|
14019
|
+
};
|
|
14020
|
+
const startWatching = () => {
|
|
14021
|
+
try {
|
|
14022
|
+
watchHandle = (params.dependencies?.watchRoot ?? watchDriveRoot)(params.rootDir, (reason, dirtyPath) => {
|
|
14023
|
+
void requestSync(reason, {
|
|
14024
|
+
dirtyLocalPaths: dirtyPath ? [dirtyPath] : void 0
|
|
14025
|
+
});
|
|
14026
|
+
}, (error) => {
|
|
14027
|
+
const normalizedError = asError(error);
|
|
14028
|
+
watchStatus = "error";
|
|
14029
|
+
watchError = normalizedError.message;
|
|
14030
|
+
console.error(`${MANAGED_GATEWAY_LOG_PREFIX} drive sync filesystem watcher failed`, {
|
|
14031
|
+
rootDir: params.rootDir,
|
|
14032
|
+
error: normalizedError
|
|
14033
|
+
});
|
|
14034
|
+
});
|
|
14035
|
+
watchStatus = watchHandle ? "active" : "unavailable";
|
|
14036
|
+
watchError = watchHandle ? null : "filesystem watcher unavailable";
|
|
14037
|
+
} catch (error) {
|
|
14038
|
+
const normalizedError = asError(error);
|
|
14039
|
+
watchStatus = "unavailable";
|
|
14040
|
+
watchError = normalizedError.message;
|
|
14041
|
+
console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} drive sync filesystem watcher unavailable`, {
|
|
14042
|
+
rootDir: params.rootDir,
|
|
14043
|
+
error: normalizedError
|
|
14044
|
+
});
|
|
14045
|
+
}
|
|
14046
|
+
};
|
|
14047
|
+
startWatching();
|
|
14048
|
+
pollTimer = setInterval(() => {
|
|
14049
|
+
void requestSync("drive_sync_poll", { immediate: true });
|
|
14050
|
+
}, params.pollIntervalMs);
|
|
14051
|
+
pollTimer.unref?.();
|
|
14052
|
+
return {
|
|
14053
|
+
requestSync,
|
|
14054
|
+
getStatus() {
|
|
14055
|
+
return {
|
|
14056
|
+
running,
|
|
14057
|
+
pending: pendingReason !== null,
|
|
14058
|
+
watchStatus,
|
|
14059
|
+
lastReason,
|
|
14060
|
+
lastStartedAt,
|
|
14061
|
+
lastFinishedAt,
|
|
14062
|
+
lastError,
|
|
14063
|
+
watchError,
|
|
14064
|
+
lastSummary
|
|
14065
|
+
};
|
|
14066
|
+
},
|
|
14067
|
+
async stop() {
|
|
14068
|
+
stopped = true;
|
|
14069
|
+
clearDebounceTimer();
|
|
14070
|
+
if (pollTimer) {
|
|
14071
|
+
clearInterval(pollTimer);
|
|
14072
|
+
pollTimer = null;
|
|
14073
|
+
}
|
|
14074
|
+
watchHandle?.close();
|
|
14075
|
+
watchHandle = null;
|
|
14076
|
+
completeWaiters(pendingWaiters, { status: "skipped", reason: "scheduler_stopped" });
|
|
14077
|
+
pendingWaiters = [];
|
|
14078
|
+
pendingReason = null;
|
|
14079
|
+
pendingRequiresLocalWrites = false;
|
|
14080
|
+
pendingRequiresFullLocalScan = false;
|
|
14081
|
+
pendingDirtyLocalPaths.clear();
|
|
14082
|
+
await currentRun;
|
|
14083
|
+
}
|
|
14084
|
+
};
|
|
14085
|
+
}
|
|
14086
|
+
function requestRequiresLocalWrites(params) {
|
|
14087
|
+
if (params.reason !== "drive_sync_poll") {
|
|
14088
|
+
return true;
|
|
14089
|
+
}
|
|
14090
|
+
if (!params.hasCompletedLocalWriteSync && params.watchStatus !== "active") {
|
|
14091
|
+
return true;
|
|
14092
|
+
}
|
|
14093
|
+
return Date.now() - params.lastSuccessfulLocalWriteSyncAt >= params.localAuditIntervalMs;
|
|
14094
|
+
}
|
|
14095
|
+
function watchDriveRoot(rootDir, onChange, onError) {
|
|
14096
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
14097
|
+
let closed = false;
|
|
14098
|
+
let refreshTimer = null;
|
|
14099
|
+
const addDirectoryTree = (directoryPath) => {
|
|
14100
|
+
if (closed || watchers.has(directoryPath)) {
|
|
14101
|
+
return;
|
|
14102
|
+
}
|
|
14103
|
+
const directoryStat = lstatSync(directoryPath);
|
|
14104
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
|
|
14105
|
+
return;
|
|
14106
|
+
}
|
|
14107
|
+
const watcher = watch(directoryPath, (_eventType, filename) => {
|
|
14108
|
+
const dirtyPath = typeof filename === "string" || Buffer.isBuffer(filename) ? path19.join(directoryPath, filename.toString()) : directoryPath;
|
|
14109
|
+
onChange("drive_sync_filesystem_change", dirtyPath);
|
|
14110
|
+
scheduleRefresh();
|
|
14111
|
+
});
|
|
14112
|
+
watcher.on("error", (error) => {
|
|
14113
|
+
watchers.delete(directoryPath);
|
|
14114
|
+
onError(asError(error));
|
|
14115
|
+
});
|
|
14116
|
+
watchers.set(directoryPath, watcher);
|
|
14117
|
+
for (const child of readdirSync(directoryPath, { withFileTypes: true })) {
|
|
14118
|
+
if (!child.isDirectory()) {
|
|
14119
|
+
continue;
|
|
14120
|
+
}
|
|
14121
|
+
addDirectoryTree(path19.join(directoryPath, child.name));
|
|
14122
|
+
}
|
|
14123
|
+
};
|
|
14124
|
+
const refreshDirectoryTree = () => {
|
|
14125
|
+
if (closed) {
|
|
14126
|
+
return;
|
|
14127
|
+
}
|
|
14128
|
+
try {
|
|
14129
|
+
addDirectoryTree(rootDir);
|
|
14130
|
+
} catch (error) {
|
|
14131
|
+
onError(asError(error));
|
|
14132
|
+
}
|
|
14133
|
+
};
|
|
14134
|
+
const scheduleRefresh = () => {
|
|
14135
|
+
if (closed || refreshTimer) {
|
|
14136
|
+
return;
|
|
14137
|
+
}
|
|
14138
|
+
refreshTimer = setTimeout(() => {
|
|
14139
|
+
refreshTimer = null;
|
|
14140
|
+
refreshDirectoryTree();
|
|
14141
|
+
}, 250);
|
|
14142
|
+
refreshTimer.unref?.();
|
|
14143
|
+
};
|
|
14144
|
+
addDirectoryTree(rootDir);
|
|
14145
|
+
return {
|
|
14146
|
+
close() {
|
|
14147
|
+
closed = true;
|
|
14148
|
+
if (refreshTimer) {
|
|
14149
|
+
clearTimeout(refreshTimer);
|
|
14150
|
+
refreshTimer = null;
|
|
14151
|
+
}
|
|
14152
|
+
for (const watcher of watchers.values()) {
|
|
14153
|
+
watcher.close();
|
|
14154
|
+
}
|
|
14155
|
+
watchers.clear();
|
|
14156
|
+
}
|
|
14157
|
+
};
|
|
14158
|
+
}
|
|
14159
|
+
|
|
11058
14160
|
// dist/managed-runtime/heartbeat.js
|
|
11059
14161
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
11060
14162
|
function startHostHeartbeatLoop(params) {
|
|
@@ -11201,6 +14303,8 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
|
|
|
11201
14303
|
let stopHeartbeat = null;
|
|
11202
14304
|
let stopRealtimeWakeLoop = null;
|
|
11203
14305
|
let unsubscribeAuthStateChange = null;
|
|
14306
|
+
let driveSyncService = null;
|
|
14307
|
+
let driveSyncScheduler = null;
|
|
11204
14308
|
const removeSignalHandlers = runtimeDependencies.registerSignalHandlers(onSignal);
|
|
11205
14309
|
try {
|
|
11206
14310
|
await heartbeatHost();
|
|
@@ -11219,6 +14323,30 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
|
|
|
11219
14323
|
controlTimeoutMs: config.controlTimeoutMs
|
|
11220
14324
|
});
|
|
11221
14325
|
unsubscribeAuthStateChange = realtimeClient.unsubscribeAuthStateChange;
|
|
14326
|
+
if (config.driveSync.enabled) {
|
|
14327
|
+
driveSyncService = await runtimeDependencies.createDriveSyncService({
|
|
14328
|
+
config,
|
|
14329
|
+
supabase: realtimeClient.supabase
|
|
14330
|
+
});
|
|
14331
|
+
console.log(`${MANAGED_GATEWAY_LOG_PREFIX} enabled managed drive sync`, {
|
|
14332
|
+
rootDir: config.driveSync.rootDir,
|
|
14333
|
+
stateDir: config.driveSync.stateDir,
|
|
14334
|
+
clientKey: config.driveSync.clientKey ?? "<local-state-derived>",
|
|
14335
|
+
changeLimit: config.driveSync.changeLimit,
|
|
14336
|
+
maxBatchesPerTick: config.driveSync.maxBatchesPerTick,
|
|
14337
|
+
debounceMs: config.driveSync.debounceMs,
|
|
14338
|
+
pollIntervalMs: config.driveSync.pollIntervalMs,
|
|
14339
|
+
localAuditIntervalMs: config.driveSync.localAuditIntervalMs
|
|
14340
|
+
});
|
|
14341
|
+
await mkdir4(config.driveSync.rootDir, { recursive: true });
|
|
14342
|
+
driveSyncScheduler = startManagedDriveSyncScheduler({
|
|
14343
|
+
service: driveSyncService,
|
|
14344
|
+
rootDir: config.driveSync.rootDir,
|
|
14345
|
+
debounceMs: config.driveSync.debounceMs,
|
|
14346
|
+
pollIntervalMs: config.driveSync.pollIntervalMs,
|
|
14347
|
+
localAuditIntervalMs: config.driveSync.localAuditIntervalMs
|
|
14348
|
+
});
|
|
14349
|
+
}
|
|
11222
14350
|
stopRealtimeWakeLoop = runtimeDependencies.startRealtimeWakeLoop({
|
|
11223
14351
|
supabase: realtimeClient.supabase,
|
|
11224
14352
|
hostId: config.hostId,
|
|
@@ -11271,9 +14399,22 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
|
|
|
11271
14399
|
if (draining) {
|
|
11272
14400
|
continue;
|
|
11273
14401
|
}
|
|
14402
|
+
if (driveSyncScheduler && !activeExecutionId) {
|
|
14403
|
+
const syncResult = await driveSyncScheduler.requestSync("runtime_wake", { immediate: true });
|
|
14404
|
+
if (syncResult.status === "failed") {
|
|
14405
|
+
console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} continuing managed execution after drive sync failure`, {
|
|
14406
|
+
error: syncResult.error
|
|
14407
|
+
});
|
|
14408
|
+
} else if (syncResult.status === "degraded") {
|
|
14409
|
+
console.warn(`${MANAGED_GATEWAY_LOG_PREFIX} continuing managed execution with degraded drive sync`, {
|
|
14410
|
+
error: syncResult.error,
|
|
14411
|
+
failedDriveCount: syncResult.summary.failedDriveCount
|
|
14412
|
+
});
|
|
14413
|
+
}
|
|
14414
|
+
}
|
|
11274
14415
|
while (!shutdownRequested && !draining) {
|
|
11275
14416
|
try {
|
|
11276
|
-
const claimToken =
|
|
14417
|
+
const claimToken = randomUUID8();
|
|
11277
14418
|
const execution = await runtimeDependencies.dispatchLinuxExecution({
|
|
11278
14419
|
controlUrl: config.hostControlUrl,
|
|
11279
14420
|
controlToken: config.hostControlToken,
|
|
@@ -11321,6 +14462,8 @@ async function startManagedGatewayRuntime(config = resolveManagedGatewayRuntimeC
|
|
|
11321
14462
|
} finally {
|
|
11322
14463
|
stopHeartbeat?.();
|
|
11323
14464
|
await stopRealtimeWakeLoop?.();
|
|
14465
|
+
await driveSyncScheduler?.stop();
|
|
14466
|
+
driveSyncService?.close();
|
|
11324
14467
|
unsubscribeAuthStateChange?.();
|
|
11325
14468
|
removeSignalHandlers();
|
|
11326
14469
|
console.log(`${MANAGED_GATEWAY_LOG_PREFIX} stopped managed gateway runtime`, {
|
|
@@ -11464,20 +14607,20 @@ function resolveGatewayPaths2(options) {
|
|
|
11464
14607
|
return resolveGatewayPaths(getActiveOptions(options));
|
|
11465
14608
|
}
|
|
11466
14609
|
function resolveGatewayFallbackConfigDir() {
|
|
11467
|
-
if (
|
|
11468
|
-
return
|
|
14610
|
+
if (process19.platform === "darwin") {
|
|
14611
|
+
return path20.join(os8.homedir(), "Library", "Application Support", "Panorama Gateway");
|
|
11469
14612
|
}
|
|
11470
|
-
if (
|
|
11471
|
-
const base =
|
|
11472
|
-
return
|
|
14613
|
+
if (process19.platform === "win32") {
|
|
14614
|
+
const base = process19.env.APPDATA || path20.join(os8.homedir(), "AppData", "Roaming");
|
|
14615
|
+
return path20.join(base, "Panorama Gateway");
|
|
11473
14616
|
}
|
|
11474
|
-
const dataHome =
|
|
11475
|
-
return
|
|
14617
|
+
const dataHome = process19.env.XDG_DATA_HOME || path20.join(os8.homedir(), ".local", "share");
|
|
14618
|
+
return path20.join(dataHome, "panorama-gateway");
|
|
11476
14619
|
}
|
|
11477
14620
|
async function ensureWritableDirectory(dir) {
|
|
11478
14621
|
try {
|
|
11479
14622
|
await ensureOwnerOnlyDirectory(dir);
|
|
11480
|
-
const probe =
|
|
14623
|
+
const probe = path20.join(dir, `.panorama-write-${randomUUID9()}`);
|
|
11481
14624
|
await writeOwnerOnlyFile(probe, "ok");
|
|
11482
14625
|
await fs6.unlink(probe);
|
|
11483
14626
|
return { ok: true };
|
|
@@ -11487,16 +14630,16 @@ async function ensureWritableDirectory(dir) {
|
|
|
11487
14630
|
}
|
|
11488
14631
|
async function prepareGatewayConfigDir(options, allowFallback) {
|
|
11489
14632
|
const resolvedOptions = getActiveOptions(options);
|
|
11490
|
-
const overrideDir = getStringOption(resolvedOptions, "config-dir") ||
|
|
11491
|
-
const overridePath = getStringOption(resolvedOptions, "config-path") ||
|
|
14633
|
+
const overrideDir = getStringOption(resolvedOptions, "config-dir") || process19.env.PANORAMA_GATEWAY_CONFIG_DIR;
|
|
14634
|
+
const overridePath = getStringOption(resolvedOptions, "config-path") || process19.env.PANORAMA_GATEWAY_CONFIG_PATH;
|
|
11492
14635
|
const explicitOverride = Boolean(overrideDir || overridePath);
|
|
11493
14636
|
const { configDir, configPath, logPath, pidPath } = resolveGatewayPaths2(resolvedOptions);
|
|
11494
14637
|
const writable = await ensureWritableDirectory(configDir);
|
|
11495
14638
|
if (writable.ok) {
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
|
|
11499
|
-
|
|
14639
|
+
process19.env.PANORAMA_GATEWAY_CONFIG_DIR = configDir;
|
|
14640
|
+
process19.env.PANORAMA_GATEWAY_CONFIG_PATH = configPath;
|
|
14641
|
+
process19.env.PANORAMA_GATEWAY_LOG_PATH = logPath;
|
|
14642
|
+
process19.env.PANORAMA_GATEWAY_PID_PATH = pidPath;
|
|
11500
14643
|
try {
|
|
11501
14644
|
await ensureGatewayStatePermissions({ configDir, configPath, logPath, pidPath });
|
|
11502
14645
|
} catch (error) {
|
|
@@ -11517,7 +14660,7 @@ async function prepareGatewayConfigDir(options, allowFallback) {
|
|
|
11517
14660
|
const fallbackPaths = resolveGatewayPaths2(fallbackOptions);
|
|
11518
14661
|
if (configPath !== fallbackPaths.configPath && fsSync9.existsSync(configPath)) {
|
|
11519
14662
|
try {
|
|
11520
|
-
await ensureOwnerOnlyDirectory(
|
|
14663
|
+
await ensureOwnerOnlyDirectory(path20.dirname(fallbackPaths.configPath));
|
|
11521
14664
|
await fs6.copyFile(configPath, fallbackPaths.configPath);
|
|
11522
14665
|
await ensureOwnerOnlyFile(fallbackPaths.configPath);
|
|
11523
14666
|
} catch (error) {
|
|
@@ -11529,10 +14672,10 @@ async function prepareGatewayConfigDir(options, allowFallback) {
|
|
|
11529
14672
|
}
|
|
11530
14673
|
}
|
|
11531
14674
|
resolvedOptions["config-dir"] = fallbackDir;
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11535
|
-
|
|
14675
|
+
process19.env.PANORAMA_GATEWAY_CONFIG_DIR = fallbackDir;
|
|
14676
|
+
process19.env.PANORAMA_GATEWAY_CONFIG_PATH = fallbackPaths.configPath;
|
|
14677
|
+
process19.env.PANORAMA_GATEWAY_LOG_PATH = fallbackPaths.logPath;
|
|
14678
|
+
process19.env.PANORAMA_GATEWAY_PID_PATH = fallbackPaths.pidPath;
|
|
11536
14679
|
try {
|
|
11537
14680
|
await ensureGatewayStatePermissions(fallbackPaths);
|
|
11538
14681
|
} catch (error) {
|
|
@@ -11620,20 +14763,20 @@ async function getProcessUptime(pid) {
|
|
|
11620
14763
|
function findRepoRoot(startDir) {
|
|
11621
14764
|
let current = startDir;
|
|
11622
14765
|
while (true) {
|
|
11623
|
-
if (fsSync9.existsSync(
|
|
14766
|
+
if (fsSync9.existsSync(path20.join(current, "pnpm-workspace.yaml"))) {
|
|
11624
14767
|
return current;
|
|
11625
14768
|
}
|
|
11626
|
-
const parent =
|
|
14769
|
+
const parent = path20.dirname(current);
|
|
11627
14770
|
if (parent === current)
|
|
11628
14771
|
return null;
|
|
11629
14772
|
current = parent;
|
|
11630
14773
|
}
|
|
11631
14774
|
}
|
|
11632
14775
|
function loadEnvironment(options) {
|
|
11633
|
-
const envFileOption = getStringOption(options, "env-file") ||
|
|
11634
|
-
const envNameOption = getStringOption(options, "env") ||
|
|
14776
|
+
const envFileOption = getStringOption(options, "env-file") || process19.env.PANORAMA_ENV_FILE || process19.env.PANORAMA_ENV_PATH;
|
|
14777
|
+
const envNameOption = getStringOption(options, "env") || process19.env.PANORAMA_ENV || process19.env.ENVIRONMENT;
|
|
11635
14778
|
const envName = normalizeEnvName(envNameOption) ?? "local";
|
|
11636
|
-
const envPath = envFileOption ?
|
|
14779
|
+
const envPath = envFileOption ? path20.resolve(envFileOption) : path20.join(findRepoRoot(process19.cwd()) ?? process19.cwd(), envName === "local" ? ".env" : `.env.${envName}`);
|
|
11637
14780
|
if (!fsSync9.existsSync(envPath)) {
|
|
11638
14781
|
if (envFileOption || envNameOption) {
|
|
11639
14782
|
throw new GatewayCliError("Environment file not found.", `Path: ${envPath}`);
|
|
@@ -11654,7 +14797,7 @@ function shouldEmitDebugOutput(stream) {
|
|
|
11654
14797
|
return isVerbose();
|
|
11655
14798
|
}
|
|
11656
14799
|
function logInfo(message, data) {
|
|
11657
|
-
if (!shouldEmitDebugOutput(
|
|
14800
|
+
if (!shouldEmitDebugOutput(process19.stdout))
|
|
11658
14801
|
return;
|
|
11659
14802
|
if (data) {
|
|
11660
14803
|
console.log(`[gateway] ${message}`, data);
|
|
@@ -11663,7 +14806,7 @@ function logInfo(message, data) {
|
|
|
11663
14806
|
}
|
|
11664
14807
|
}
|
|
11665
14808
|
function logError(message, data) {
|
|
11666
|
-
if (!shouldEmitDebugOutput(
|
|
14809
|
+
if (!shouldEmitDebugOutput(process19.stderr))
|
|
11667
14810
|
return;
|
|
11668
14811
|
if (data) {
|
|
11669
14812
|
console.error(`[gateway] ${message}`, data);
|
|
@@ -11672,7 +14815,7 @@ function logError(message, data) {
|
|
|
11672
14815
|
}
|
|
11673
14816
|
}
|
|
11674
14817
|
function logWarn(message, data) {
|
|
11675
|
-
if (!shouldEmitDebugOutput(
|
|
14818
|
+
if (!shouldEmitDebugOutput(process19.stderr))
|
|
11676
14819
|
return;
|
|
11677
14820
|
if (data) {
|
|
11678
14821
|
console.warn(`[gateway] ${message}`, data);
|
|
@@ -11809,7 +14952,7 @@ function buildGatewayDoctorDependencies() {
|
|
|
11809
14952
|
};
|
|
11810
14953
|
}
|
|
11811
14954
|
async function run() {
|
|
11812
|
-
const parsed = parseArgs(
|
|
14955
|
+
const parsed = parseArgs(process19.argv.slice(2));
|
|
11813
14956
|
ACTIVE_OPTIONS = parsed.options;
|
|
11814
14957
|
applyOptionEnvOverrides(parsed.options);
|
|
11815
14958
|
if (parsed.options.h || parsed.options.help || parsed.command === null) {
|
|
@@ -11825,7 +14968,7 @@ async function run() {
|
|
|
11825
14968
|
throw new Error("Pairing code is required");
|
|
11826
14969
|
}
|
|
11827
14970
|
await pairGatewayCommand(code, parsed.options, configResolution, buildGatewayPairingDependencies());
|
|
11828
|
-
const entryPath =
|
|
14971
|
+
const entryPath = process19.argv[1] || "";
|
|
11829
14972
|
if (entryPath.endsWith(".ts")) {
|
|
11830
14973
|
cliInfo2('Auto-start skipped in dev mode; run "panorama-gateway start" when ready.', parsed.options);
|
|
11831
14974
|
return;
|
|
@@ -11876,7 +15019,7 @@ async function run() {
|
|
|
11876
15019
|
return;
|
|
11877
15020
|
}
|
|
11878
15021
|
printHelp();
|
|
11879
|
-
|
|
15022
|
+
process19.exitCode = 1;
|
|
11880
15023
|
}
|
|
11881
15024
|
run().catch((error) => {
|
|
11882
15025
|
const { message, details } = describeError(error);
|
|
@@ -11885,6 +15028,6 @@ run().catch((error) => {
|
|
|
11885
15028
|
detailItems.push({ label: "Details", value: details, verboseOnly: true });
|
|
11886
15029
|
}
|
|
11887
15030
|
cliError2(message, ACTIVE_OPTIONS ?? void 0, detailItems);
|
|
11888
|
-
|
|
15031
|
+
process19.exitCode = 1;
|
|
11889
15032
|
});
|
|
11890
15033
|
//# sourceMappingURL=index.js.map
|