@indigoai-us/hq-cloud 6.18.27 → 6.18.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/rescue-breaker.test.js +204 -4
- package/dist/cli/rescue-breaker.test.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +6 -1
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +258 -151
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-preserve-copy-symlink.test.js +3 -8
- package/dist/cli/rescue-preserve-copy-symlink.test.js.map +1 -1
- package/dist/cli/rescue-rsync-status.test.js +28 -2
- package/dist/cli/rescue-rsync-status.test.js.map +1 -1
- package/dist/cli/rescue-snapshot-copy.test.js +147 -41
- package/dist/cli/rescue-snapshot-copy.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +5 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +40 -6
- package/dist/cognito-auth.js.map +1 -1
- package/dist/cognito-auth.test.js +45 -0
- package/dist/cognito-auth.test.js.map +1 -1
- package/dist/outposts/command.d.ts.map +1 -1
- package/dist/outposts/command.js +26 -5
- package/dist/outposts/command.js.map +1 -1
- package/dist/outposts/command.test.js +30 -0
- package/dist/outposts/command.test.js.map +1 -1
- package/dist/outposts/session-host-auth.d.ts +3 -2
- package/dist/outposts/session-host-auth.d.ts.map +1 -1
- package/dist/outposts/session-host-command.d.ts +5 -0
- package/dist/outposts/session-host-command.d.ts.map +1 -1
- package/dist/outposts/session-host-command.js +11 -6
- package/dist/outposts/session-host-command.js.map +1 -1
- package/dist/outposts/session-host-command.test.js +52 -2
- package/dist/outposts/session-host-command.test.js.map +1 -1
- package/dist/outposts/session-host-fakes.d.ts +1 -1
- package/dist/outposts/session-host-fakes.d.ts.map +1 -1
- package/dist/outposts/session-host-fakes.js +1 -1
- package/dist/outposts/session-host-fakes.js.map +1 -1
- package/dist/outposts/session-host-protocol.d.ts +11 -12
- package/dist/outposts/session-host-protocol.d.ts.map +1 -1
- package/dist/outposts/session-host-protocol.js +1 -1
- package/dist/outposts/session-host-protocol.js.map +1 -1
- package/dist/outposts/session-host-sessions.d.ts +2 -1
- package/dist/outposts/session-host-sessions.d.ts.map +1 -1
- package/dist/signals/internals.d.ts +0 -6
- package/dist/signals/internals.d.ts.map +1 -1
- package/dist/signals/internals.js +3 -14
- package/dist/signals/internals.js.map +1 -1
- package/dist/sources/internals.d.ts +0 -4
- package/dist/sources/internals.d.ts.map +1 -1
- package/dist/sources/internals.js +3 -3
- package/dist/sources/internals.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/rescue-core.js
CHANGED
|
@@ -149,6 +149,8 @@ const GIT_NETWORK_RETRY_DELAYS_MS = [1_000, 2_000];
|
|
|
149
149
|
// libcurl's low-speed pair (see gitNetworkEnv) aborts a transfer that stalls,
|
|
150
150
|
// and a slow transfer that is still moving is allowed to finish.
|
|
151
151
|
const GIT_REACHABILITY_PROBE_TIMEOUT_MS = 90_000;
|
|
152
|
+
const SNAPSHOT_CLONE_METADATA_ALLOWANCE_BYTES = 64 * 1024 ** 2;
|
|
153
|
+
const CLOUD_UPDATE_OVERLAY_FREE_SPACE_FLOOR_BYTES = 16 * 1024 ** 2;
|
|
152
154
|
function gitNetworkEnv(env) {
|
|
153
155
|
return {
|
|
154
156
|
...env,
|
|
@@ -731,7 +733,7 @@ export function runRescue(argv, opts = {}) {
|
|
|
731
733
|
tmpdir = d;
|
|
732
734
|
}, () => {
|
|
733
735
|
retainTmpdir = true;
|
|
734
|
-
}, opts.gitReachabilityProbeTimeoutMs ?? GIT_REACHABILITY_PROBE_TIMEOUT_MS, opts.coreSourceStampWriter, opts.restoreOnRsyncFailure === true, opts.backupCapacityStatfsSync ?? fs.statfsSync);
|
|
736
|
+
}, opts.gitReachabilityProbeTimeoutMs ?? GIT_REACHABILITY_PROBE_TIMEOUT_MS, opts.coreSourceStampWriter, opts.restoreOnRsyncFailure === true, opts.backupCapacityStatfsSync ?? fs.statfsSync, opts.backupCapacityCloneProbe ?? probeCopyOnWriteClone, opts.backupCapacityStatSync ?? fs.statSync);
|
|
735
737
|
}
|
|
736
738
|
catch (e) {
|
|
737
739
|
if (e instanceof ExitError)
|
|
@@ -1463,7 +1465,7 @@ function hashRecoveryTree(root, rootRel) {
|
|
|
1463
1465
|
}
|
|
1464
1466
|
return { kind: "present", digest: hash.digest("hex") };
|
|
1465
1467
|
}
|
|
1466
|
-
function rsyncOutputHasOnlyDirectoryChanges(output) {
|
|
1468
|
+
export function rsyncOutputHasOnlyDirectoryChanges(output) {
|
|
1467
1469
|
for (const rawLine of output.split(/\r?\n/)) {
|
|
1468
1470
|
const line = rawLine.trim();
|
|
1469
1471
|
if (!line)
|
|
@@ -1476,9 +1478,20 @@ function rsyncOutputHasOnlyDirectoryChanges(output) {
|
|
|
1476
1478
|
}
|
|
1477
1479
|
if (line.startsWith("*deleting "))
|
|
1478
1480
|
return false;
|
|
1479
|
-
const itemized = /^([<>ch.*])([fdLDS])[
|
|
1480
|
-
if (!itemized
|
|
1481
|
+
const itemized = /^([<>ch.*])([fdLDS])([.cstpoguaxT]{7}(?:[.cstpoguaxT]{2})?)\s+/.exec(line);
|
|
1482
|
+
if (!itemized)
|
|
1481
1483
|
return false;
|
|
1484
|
+
if (itemized[2] === "d")
|
|
1485
|
+
continue;
|
|
1486
|
+
// `--checksum --no-times` can still itemize differing mtimes as `.f..T....`.
|
|
1487
|
+
// Checksum comparison has already proved the file bytes match; ignore that
|
|
1488
|
+
// metadata-only difference while keeping every content change fail-closed.
|
|
1489
|
+
if (itemized[1] === "." &&
|
|
1490
|
+
itemized[2] === "f" &&
|
|
1491
|
+
[...itemized[3]].every((flag, index) => flag === "." || (index === 2 && flag === "T"))) {
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
return false;
|
|
1482
1495
|
}
|
|
1483
1496
|
return true;
|
|
1484
1497
|
}
|
|
@@ -1523,7 +1536,7 @@ function recoveryProof(unresolved, cfg, hqRoot, srcDir, srcSha, rsyncLauncher, r
|
|
|
1523
1536
|
}
|
|
1524
1537
|
return { eligible: !hasSkippedSnapshotEntries(newest.entry.manifest), failures };
|
|
1525
1538
|
}
|
|
1526
|
-
function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTimeoutMs = GIT_REACHABILITY_PROBE_TIMEOUT_MS, stampWriter = writeCoreSourceStamp, restoreOnRsyncFailure = false, backupCapacityStatfsSync = fs.statfsSync) {
|
|
1539
|
+
function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTimeoutMs = GIT_REACHABILITY_PROBE_TIMEOUT_MS, stampWriter = writeCoreSourceStamp, restoreOnRsyncFailure = false, backupCapacityStatfsSync = fs.statfsSync, backupCapacityCloneProbe = probeCopyOnWriteClone, backupCapacityStatSync = fs.statSync) {
|
|
1527
1540
|
const hqRoot = resolveHqRoot(cfg, err);
|
|
1528
1541
|
const RUN_TS = utcStamp(new Date(), "dash");
|
|
1529
1542
|
// Both the release overlay and a restorable snapshot require rsync. A shim
|
|
@@ -2038,103 +2051,110 @@ function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTim
|
|
|
2038
2051
|
}
|
|
2039
2052
|
}
|
|
2040
2053
|
}
|
|
2041
|
-
requireBackupCapacity(cfg, hqRoot, snapshotRoots, env, err, backupCapacityStatfsSync);
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2054
|
+
const snapshotCapacityAvailable = requireBackupCapacity(cfg, hqRoot, snapshotRoots, srcDir, env, err, backupCapacityStatfsSync, backupCapacityCloneProbe, backupCapacityStatSync);
|
|
2055
|
+
if (!snapshotCapacityAvailable) {
|
|
2056
|
+
cfg.doBackup = false;
|
|
2057
|
+
out("\nwarning: safety snapshot skipped because available space cannot cover the snapshot allowance; the cloud update continues without a pre-update restore point.\n");
|
|
2058
|
+
err("HQ_RESCUE_SNAPSHOT=skipped-low-space\n");
|
|
2059
|
+
}
|
|
2060
|
+
if (snapshotCapacityAvailable) {
|
|
2061
|
+
fs.mkdirSync(cfg.backupRoot, { recursive: true });
|
|
2062
|
+
const runId = `${process.pid}-${Date.now().toString(36)}`;
|
|
2063
|
+
backupDir = allocateSnapshotDir(cfg.backupRoot, RUN_TS, canonicalRootHash(hqRoot), runId);
|
|
2064
|
+
out("\n");
|
|
2065
|
+
out(`==> Safety snapshot -> ${backupDir}\n`);
|
|
2066
|
+
const snapshotSymlinks = [];
|
|
2067
|
+
const snapshotSkippedErrors = [];
|
|
2068
|
+
try {
|
|
2069
|
+
fs.mkdirSync(backupDir, { recursive: false });
|
|
2070
|
+
writeSnapshotManifest(backupDir, snapshotManifest("copying", hqRoot, runId, cfg, srcSha, snapshotRoots));
|
|
2071
|
+
const attemptedRoots = snapshotRoots.filter((rootRel) => !isRescueTransientDirectory(rootRel));
|
|
2072
|
+
const failedRoots = [];
|
|
2073
|
+
for (const rootRel of snapshotRoots) {
|
|
2074
|
+
const srcAbs = path.join(hqRoot, rootRel);
|
|
2075
|
+
if (isRescueTransientDirectory(rootRel))
|
|
2076
|
+
continue;
|
|
2077
|
+
try {
|
|
2078
|
+
const copy = cpAToFiltered(srcAbs, path.join(backupDir, rootRel), rootRel, hqRoot, snapshotSymlinks, snapshotSkippedErrors);
|
|
2079
|
+
if (!copy.captured) {
|
|
2080
|
+
const failure = copy.firstFailure ??
|
|
2081
|
+
new SnapshotCopyError(rootRel, "metadata", new Error("snapshot root contains no capturable entries"), srcAbs);
|
|
2082
|
+
if (!copy.firstFailure)
|
|
2083
|
+
snapshotSkippedErrors.push(failure);
|
|
2084
|
+
failedRoots.push(failure);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
catch (error) {
|
|
2088
|
+
if (error instanceof SnapshotCopyError) {
|
|
2089
|
+
snapshotSkippedErrors.push(error);
|
|
2090
|
+
failedRoots.push(error);
|
|
2091
|
+
if (error.code === "ENOSPC")
|
|
2092
|
+
break;
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
throw error;
|
|
2066
2096
|
}
|
|
2067
2097
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2098
|
+
snapshotSkippedEntries = snapshotSkippedErrors.map(snapshotCopyFailure);
|
|
2099
|
+
const diskFull = snapshotSkippedErrors.find((error) => error.code === "ENOSPC");
|
|
2100
|
+
if (diskFull) {
|
|
2101
|
+
reportSnapshotCopyFailure(diskFull, err);
|
|
2102
|
+
try {
|
|
2103
|
+
updateSnapshotStatus(backupDir, "failed-pre-mutation", undefined, {
|
|
2104
|
+
symlinks: snapshotSymlinks,
|
|
2105
|
+
copyFailure: snapshotCopyFailure(diskFull),
|
|
2106
|
+
skippedEntries: snapshotSkippedEntries,
|
|
2107
|
+
});
|
|
2075
2108
|
}
|
|
2076
|
-
|
|
2109
|
+
catch (error) {
|
|
2110
|
+
err(`warning: could not record the failed safety snapshot while disk space is exhausted (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
2111
|
+
err("HQ_RESCUE_MANIFEST_UPDATE_FAILED=1\n");
|
|
2112
|
+
}
|
|
2113
|
+
return { status: 1 };
|
|
2077
2114
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
if (diskFull) {
|
|
2082
|
-
reportSnapshotCopyFailure(diskFull, err);
|
|
2083
|
-
try {
|
|
2115
|
+
if (attemptedRoots.length > 0 &&
|
|
2116
|
+
failedRoots.length === attemptedRoots.length) {
|
|
2117
|
+
const failure = failedRoots[0];
|
|
2084
2118
|
updateSnapshotStatus(backupDir, "failed-pre-mutation", undefined, {
|
|
2085
2119
|
symlinks: snapshotSymlinks,
|
|
2086
|
-
copyFailure: snapshotCopyFailure(
|
|
2120
|
+
copyFailure: snapshotCopyFailure(failure),
|
|
2087
2121
|
skippedEntries: snapshotSkippedEntries,
|
|
2088
2122
|
});
|
|
2123
|
+
reportSnapshotCopyFailure(failure, err);
|
|
2124
|
+
throw new ExitError(1);
|
|
2089
2125
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
err("HQ_RESCUE_MANIFEST_UPDATE_FAILED=1\n");
|
|
2126
|
+
for (const error of snapshotSkippedErrors) {
|
|
2127
|
+
reportSkippedSnapshotCopy(error, err);
|
|
2093
2128
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (attemptedRoots.length > 0 &&
|
|
2097
|
-
failedRoots.length === attemptedRoots.length) {
|
|
2098
|
-
const failure = failedRoots[0];
|
|
2099
|
-
updateSnapshotStatus(backupDir, "failed-pre-mutation", undefined, {
|
|
2129
|
+
protectSkippedSnapshotPaths(ctx, snapshotSkippedEntries);
|
|
2130
|
+
updateSnapshotStatus(backupDir, snapshotSkippedEntries.length > 0 ? "incomplete" : "ready", undefined, {
|
|
2100
2131
|
symlinks: snapshotSymlinks,
|
|
2101
|
-
copyFailure: snapshotCopyFailure(failure),
|
|
2102
2132
|
skippedEntries: snapshotSkippedEntries,
|
|
2103
2133
|
});
|
|
2104
|
-
reportSnapshotCopyFailure(failure, err);
|
|
2105
|
-
throw new ExitError(1);
|
|
2106
2134
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
if (error instanceof SnapshotCopyError) {
|
|
2135
|
+
catch (error) {
|
|
2136
|
+
if (error instanceof SnapshotCopyError) {
|
|
2137
|
+
updateSnapshotStatus(backupDir, "failed-pre-mutation", undefined, {
|
|
2138
|
+
symlinks: snapshotSymlinks,
|
|
2139
|
+
copyFailure: snapshotCopyFailure(error),
|
|
2140
|
+
skippedEntries: snapshotSkippedEntries,
|
|
2141
|
+
});
|
|
2142
|
+
reportSnapshotCopyFailure(error, err);
|
|
2143
|
+
throw new ExitError(1);
|
|
2144
|
+
}
|
|
2118
2145
|
updateSnapshotStatus(backupDir, "failed-pre-mutation", undefined, {
|
|
2119
2146
|
symlinks: snapshotSymlinks,
|
|
2120
|
-
copyFailure: snapshotCopyFailure(error),
|
|
2121
2147
|
skippedEntries: snapshotSkippedEntries,
|
|
2122
2148
|
});
|
|
2123
|
-
|
|
2124
|
-
|
|
2149
|
+
throw error;
|
|
2150
|
+
}
|
|
2151
|
+
pruneManagedBackups(cfg, hqRoot, out, true);
|
|
2152
|
+
if (snapshotSkippedEntries.length > 0) {
|
|
2153
|
+
out(" snapshot incomplete: skipped files were left untouched and this snapshot cannot restore every captured root.\n");
|
|
2154
|
+
}
|
|
2155
|
+
else {
|
|
2156
|
+
out(" snapshot ready (complete recovery is available through --recover-snapshots).\n");
|
|
2125
2157
|
}
|
|
2126
|
-
updateSnapshotStatus(backupDir, "failed-pre-mutation", undefined, {
|
|
2127
|
-
symlinks: snapshotSymlinks,
|
|
2128
|
-
skippedEntries: snapshotSkippedEntries,
|
|
2129
|
-
});
|
|
2130
|
-
throw error;
|
|
2131
|
-
}
|
|
2132
|
-
pruneManagedBackups(cfg, hqRoot, out, true);
|
|
2133
|
-
if (snapshotSkippedEntries.length > 0) {
|
|
2134
|
-
out(" snapshot incomplete: skipped files were left untouched and this snapshot cannot restore every captured root.\n");
|
|
2135
|
-
}
|
|
2136
|
-
else {
|
|
2137
|
-
out(" snapshot ready (complete recovery is available through --recover-snapshots).\n");
|
|
2138
2158
|
}
|
|
2139
2159
|
}
|
|
2140
2160
|
else if (cfg.doBackup &&
|
|
@@ -4526,43 +4546,145 @@ function estimateShuttleBytes(abs) {
|
|
|
4526
4546
|
.readdirSync(abs)
|
|
4527
4547
|
.reduce((total, name) => total + estimateShuttleBytes(path.join(abs, name)), 0);
|
|
4528
4548
|
}
|
|
4529
|
-
export function requireBackupCapacity(cfg, hqRoot, roots, _env, err, statfsSync = fs.statfsSync) {
|
|
4530
|
-
let failure;
|
|
4549
|
+
export function requireBackupCapacity(cfg, hqRoot, roots, srcDir, _env, err, statfsSync = fs.statfsSync, cloneProbe = probeCopyOnWriteClone, statSync = fs.statSync) {
|
|
4531
4550
|
try {
|
|
4532
4551
|
const estimate = roots.reduce((total, root) => total + estimateFiltered(path.join(hqRoot, root), root).bytes, 0);
|
|
4533
|
-
const
|
|
4534
|
-
const
|
|
4535
|
-
const
|
|
4552
|
+
const backupLocation = nearestExistingDirectory(cfg.backupRoot);
|
|
4553
|
+
const tempLocation = nearestExistingDirectory(os.tmpdir());
|
|
4554
|
+
const hqDevice = Number(statSync(hqRoot).dev);
|
|
4555
|
+
const backupDevice = Number(statSync(backupLocation).dev);
|
|
4556
|
+
const tempDevice = Number(statSync(tempLocation).dev);
|
|
4557
|
+
const backupFs = statfsSync(backupLocation);
|
|
4536
4558
|
const backupFree = Number(backupFs.bavail) * Number(backupFs.bsize);
|
|
4537
4559
|
const backupCapacity = Number(backupFs.blocks) * Number(backupFs.bsize);
|
|
4538
|
-
const
|
|
4560
|
+
const sameHqBackupFilesystem = hqDevice === backupDevice;
|
|
4561
|
+
const sameBackupTempFilesystem = backupDevice === tempDevice;
|
|
4562
|
+
const sameHqTempFilesystem = hqDevice === tempDevice;
|
|
4539
4563
|
const shuttleBytes = (cfg.preserveSubpaths ?? []).reduce((total, rel) => total + estimateShuttleBytes(path.join(hqRoot, rel)), 0);
|
|
4540
|
-
const
|
|
4541
|
-
const
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4564
|
+
const snapshotCloneSupported = sameHqBackupFilesystem && cloneProbe(hqRoot, cfg.backupRoot, roots);
|
|
4565
|
+
const shuttleCloneSupported = shuttleBytes > 0 &&
|
|
4566
|
+
sameHqTempFilesystem &&
|
|
4567
|
+
(sameBackupTempFilesystem && snapshotCloneSupported
|
|
4568
|
+
? true
|
|
4569
|
+
: cloneProbe(hqRoot, tempLocation, cfg.preserveSubpaths ?? []));
|
|
4570
|
+
const backupMargin = Math.min(10 * 1024 ** 3, Math.max(5 * 1024 ** 3, Math.ceil(backupCapacity * 0.1)));
|
|
4571
|
+
const snapshotRequired = snapshotCloneSupported
|
|
4572
|
+
? SNAPSHOT_CLONE_METADATA_ALLOWANCE_BYTES
|
|
4573
|
+
: estimate + backupMargin;
|
|
4574
|
+
const shuttleOnBackupRequired = sameBackupTempFilesystem
|
|
4575
|
+
? shuttleBytes === 0
|
|
4576
|
+
? 0
|
|
4577
|
+
: shuttleCloneSupported
|
|
4578
|
+
? SNAPSHOT_CLONE_METADATA_ALLOWANCE_BYTES
|
|
4579
|
+
: shuttleBytes
|
|
4580
|
+
: 0;
|
|
4581
|
+
const backupRequired = snapshotRequired + shuttleOnBackupRequired;
|
|
4582
|
+
let shuttleFree = Number.POSITIVE_INFINITY;
|
|
4583
|
+
let shuttleRequired = 0;
|
|
4584
|
+
if (!sameBackupTempFilesystem && shuttleBytes > 0) {
|
|
4585
|
+
const tempFs = statfsSync(tempLocation);
|
|
4586
|
+
shuttleFree = Number(tempFs.bavail) * Number(tempFs.bsize);
|
|
4548
4587
|
const tempCapacity = Number(tempFs.blocks) * Number(tempFs.bsize);
|
|
4549
4588
|
const tempMargin = Math.min(10 * 1024 ** 3, Math.max(5 * 1024 ** 3, Math.ceil(tempCapacity * 0.1)));
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4589
|
+
shuttleRequired = shuttleCloneSupported
|
|
4590
|
+
? SNAPSHOT_CLONE_METADATA_ALLOWANCE_BYTES
|
|
4591
|
+
: shuttleBytes + tempMargin;
|
|
4592
|
+
}
|
|
4593
|
+
const backupFits = backupFree >= backupRequired;
|
|
4594
|
+
const shuttleFits = shuttleFree >= shuttleRequired;
|
|
4595
|
+
if (backupFits && shuttleFits)
|
|
4596
|
+
return true;
|
|
4597
|
+
if (!shuttleFits) {
|
|
4598
|
+
err(`error: insufficient free space for safety snapshot (need ${shuttleRequired} bytes, have ${shuttleFree}).\n`);
|
|
4599
|
+
err("HQ_RESCUE_FAILURE_KIND=disk_full\n");
|
|
4600
|
+
throw new ExitError(1);
|
|
4601
|
+
}
|
|
4602
|
+
if (cfg.cloudUpdate) {
|
|
4603
|
+
const overlayEstimate = estimateFiltered(srcDir, "").bytes;
|
|
4604
|
+
const overlayRequired = overlayEstimate + CLOUD_UPDATE_OVERLAY_FREE_SPACE_FLOOR_BYTES;
|
|
4605
|
+
const hqFs = hqDevice === backupDevice ? backupFs : statfsSync(hqRoot);
|
|
4606
|
+
const hqFree = Number(hqFs.bavail) * Number(hqFs.bsize);
|
|
4607
|
+
const overlayOnBackupFs = hqDevice === backupDevice;
|
|
4608
|
+
const overlayNeed = overlayRequired +
|
|
4609
|
+
(overlayOnBackupFs ? shuttleOnBackupRequired : 0);
|
|
4610
|
+
const separateShuttleFits = overlayOnBackupFs || backupFree >= shuttleOnBackupRequired;
|
|
4611
|
+
if (hqFree >= overlayNeed && separateShuttleFits) {
|
|
4612
|
+
return false;
|
|
4553
4613
|
}
|
|
4614
|
+
err(`error: insufficient free space for safety snapshot (need ${overlayRequired} bytes, have ${hqFree}).\n`);
|
|
4615
|
+
err("HQ_RESCUE_FAILURE_KIND=disk_full\n");
|
|
4616
|
+
throw new ExitError(1);
|
|
4554
4617
|
}
|
|
4618
|
+
err(`error: insufficient free space for safety snapshot (need ${backupRequired} bytes, have ${backupFree}).\n`);
|
|
4619
|
+
err("HQ_RESCUE_FAILURE_KIND=disk_full\n");
|
|
4620
|
+
throw new ExitError(1);
|
|
4555
4621
|
}
|
|
4556
4622
|
catch (error) {
|
|
4623
|
+
if (error instanceof ExitError)
|
|
4624
|
+
throw error;
|
|
4557
4625
|
err(`error: could not measure snapshot size/free space (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
4558
4626
|
err("HQ_RESCUE_FAILURE_KIND=backup-capacity-check-failed\n");
|
|
4559
4627
|
throw new ExitError(1);
|
|
4560
4628
|
}
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4629
|
+
return true;
|
|
4630
|
+
}
|
|
4631
|
+
function probeCopyOnWriteClone(hqRoot, backupRoot, roots) {
|
|
4632
|
+
const source = roots
|
|
4633
|
+
.map((root) => firstRegularFile(path.join(hqRoot, root)))
|
|
4634
|
+
.find((candidate) => candidate !== null);
|
|
4635
|
+
if (!source)
|
|
4636
|
+
return false;
|
|
4637
|
+
const parent = nearestExistingDirectory(backupRoot);
|
|
4638
|
+
let probeDir;
|
|
4639
|
+
try {
|
|
4640
|
+
probeDir = fs.mkdtempSync(path.join(parent, ".hq-rescue-clone-probe-"));
|
|
4641
|
+
}
|
|
4642
|
+
catch (error) {
|
|
4643
|
+
if (error.code === "ENOSPC")
|
|
4644
|
+
return false;
|
|
4645
|
+
throw error;
|
|
4646
|
+
}
|
|
4647
|
+
try {
|
|
4648
|
+
fs.copyFileSync(source, path.join(probeDir, "probe"), fs.constants.COPYFILE_FICLONE_FORCE);
|
|
4649
|
+
return true;
|
|
4650
|
+
}
|
|
4651
|
+
catch (error) {
|
|
4652
|
+
const code = error.code;
|
|
4653
|
+
if (code === "EXDEV" ||
|
|
4654
|
+
code === "EINVAL" ||
|
|
4655
|
+
code === "ENOSYS" ||
|
|
4656
|
+
code === "ENOTSUP" ||
|
|
4657
|
+
code === "EOPNOTSUPP" ||
|
|
4658
|
+
code === "ENOTTY" ||
|
|
4659
|
+
code === "ENOSPC" ||
|
|
4660
|
+
code === "EPERM") {
|
|
4661
|
+
return false;
|
|
4662
|
+
}
|
|
4663
|
+
throw error;
|
|
4664
|
+
}
|
|
4665
|
+
finally {
|
|
4666
|
+
fs.rmSync(probeDir, { recursive: true, force: true });
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
function firstRegularFile(root, rel = "") {
|
|
4670
|
+
const st = lstatOrNull(root);
|
|
4671
|
+
if (!st || st.isSymbolicLink())
|
|
4672
|
+
return null;
|
|
4673
|
+
if (rel && isRescueTransientDirectory(rel))
|
|
4674
|
+
return null;
|
|
4675
|
+
if (st.isFile())
|
|
4676
|
+
return root;
|
|
4677
|
+
if (!st.isDirectory())
|
|
4678
|
+
return null;
|
|
4679
|
+
for (const name of fs.readdirSync(root)) {
|
|
4680
|
+
const childRel = path.posix.join(rel, name);
|
|
4681
|
+
if (isRescueTransientDirectory(childRel))
|
|
4682
|
+
continue;
|
|
4683
|
+
const found = firstRegularFile(path.join(root, name), childRel);
|
|
4684
|
+
if (found)
|
|
4685
|
+
return found;
|
|
4686
|
+
}
|
|
4687
|
+
return null;
|
|
4566
4688
|
}
|
|
4567
4689
|
function nearestExistingDirectory(target) {
|
|
4568
4690
|
let current = path.resolve(target);
|
|
@@ -4641,7 +4763,7 @@ function copySymlinkWithWindowsFallback(src, dest, link) {
|
|
|
4641
4763
|
const targetStats = fs.statSync(src);
|
|
4642
4764
|
if (targetStats.isFile()) {
|
|
4643
4765
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
4644
|
-
fs.copyFileSync(src, dest);
|
|
4766
|
+
fs.copyFileSync(src, dest, fs.constants.COPYFILE_FICLONE);
|
|
4645
4767
|
preserveSnapshotMetadata(dest, targetStats);
|
|
4646
4768
|
return;
|
|
4647
4769
|
}
|
|
@@ -4681,7 +4803,10 @@ function cpATo(src, dest) {
|
|
|
4681
4803
|
if (destinationStats && !destinationStats.isFile()) {
|
|
4682
4804
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
4683
4805
|
}
|
|
4684
|
-
|
|
4806
|
+
// The shuttle is on the same filesystem as the HQ root on common installs,
|
|
4807
|
+
// so preserve it with a clone where available. Node falls back to a full
|
|
4808
|
+
// copy on filesystems without reflink support.
|
|
4809
|
+
fs.copyFileSync(src, dest, fs.constants.COPYFILE_FICLONE);
|
|
4685
4810
|
try {
|
|
4686
4811
|
fs.chmodSync(dest, st.mode);
|
|
4687
4812
|
fs.utimesSync(dest, st.atime, st.mtime);
|
|
@@ -5132,55 +5257,37 @@ function validateSnapshotSymlink(hqRoot, rel, target) {
|
|
|
5132
5257
|
? null
|
|
5133
5258
|
: `target for ${rel} resolves outside the HQ root`;
|
|
5134
5259
|
}
|
|
5135
|
-
function copyRegularFileForSnapshot(src, dest, rel
|
|
5136
|
-
let sourceFd;
|
|
5137
|
-
let destinationFd;
|
|
5260
|
+
function copyRegularFileForSnapshot(src, dest, rel) {
|
|
5138
5261
|
try {
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
try {
|
|
5146
|
-
destinationFd = fs.openSync(dest, "w", mode);
|
|
5147
|
-
}
|
|
5148
|
-
catch (error) {
|
|
5149
|
-
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5150
|
-
}
|
|
5151
|
-
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
5152
|
-
for (;;) {
|
|
5153
|
-
let read;
|
|
5262
|
+
// Node falls back to a full copy when the filesystem cannot reflink.
|
|
5263
|
+
// Restore paths use ordinary copies, so they remain independent files.
|
|
5264
|
+
fs.copyFileSync(src, dest, fs.constants.COPYFILE_FICLONE);
|
|
5265
|
+
}
|
|
5266
|
+
catch (error) {
|
|
5267
|
+
if (error.code === "EDEADLK") {
|
|
5154
5268
|
try {
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
catch (error) {
|
|
5158
|
-
throw new SnapshotCopyError(rel, "read", error, src);
|
|
5269
|
+
fs.copyFileSync(src, dest);
|
|
5270
|
+
return;
|
|
5159
5271
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
let offset = 0;
|
|
5163
|
-
while (offset < read) {
|
|
5164
|
-
let written;
|
|
5165
|
-
try {
|
|
5166
|
-
written = fs.writeSync(destinationFd, buffer, offset, read - offset);
|
|
5167
|
-
}
|
|
5168
|
-
catch (error) {
|
|
5169
|
-
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5170
|
-
}
|
|
5171
|
-
if (written === 0) {
|
|
5172
|
-
throw new SnapshotCopyError(rel, "write", new Error("write returned zero bytes"));
|
|
5173
|
-
}
|
|
5174
|
-
offset += written;
|
|
5272
|
+
catch (fallbackError) {
|
|
5273
|
+
throw classifySnapshotCopyFailure(rel, src, dest, fallbackError);
|
|
5175
5274
|
}
|
|
5176
5275
|
}
|
|
5276
|
+
throw classifySnapshotCopyFailure(rel, src, dest, error);
|
|
5177
5277
|
}
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5278
|
+
}
|
|
5279
|
+
function classifySnapshotCopyFailure(rel, src, dest, error) {
|
|
5280
|
+
if (error.code === "EDEADLK") {
|
|
5281
|
+
return new SnapshotCopyError(rel, "read", error, src);
|
|
5282
|
+
}
|
|
5283
|
+
try {
|
|
5284
|
+
const sourceFd = fs.openSync(src, "r");
|
|
5285
|
+
fs.closeSync(sourceFd);
|
|
5286
|
+
}
|
|
5287
|
+
catch (sourceError) {
|
|
5288
|
+
return new SnapshotCopyError(rel, "read", sourceError, src);
|
|
5183
5289
|
}
|
|
5290
|
+
return new SnapshotCopyError(rel, "write", error, dest);
|
|
5184
5291
|
}
|
|
5185
5292
|
/** Preserve a Windows file link as ordinary file data when links are forbidden. */
|
|
5186
5293
|
function copySymlinkFileRepresentationForSnapshot(src, dest, rel) {
|
|
@@ -5200,7 +5307,7 @@ function copySymlinkFileRepresentationForSnapshot(src, dest, rel) {
|
|
|
5200
5307
|
catch (error) {
|
|
5201
5308
|
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5202
5309
|
}
|
|
5203
|
-
copyRegularFileForSnapshot(src, dest, rel
|
|
5310
|
+
copyRegularFileForSnapshot(src, dest, rel);
|
|
5204
5311
|
preserveSnapshotMetadata(dest, targetStats);
|
|
5205
5312
|
}
|
|
5206
5313
|
/** Snapshot copy that preserves metadata while pruning canonical transient directories. */
|
|
@@ -5285,7 +5392,7 @@ function cpAToFiltered(src, dest, rel, hqRoot, symlinks, skippedEntries) {
|
|
|
5285
5392
|
catch (error) {
|
|
5286
5393
|
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5287
5394
|
}
|
|
5288
|
-
copyRegularFileForSnapshot(src, dest, rel
|
|
5395
|
+
copyRegularFileForSnapshot(src, dest, rel);
|
|
5289
5396
|
try {
|
|
5290
5397
|
fs.chmodSync(dest, st.mode);
|
|
5291
5398
|
fs.utimesSync(dest, st.atime, st.mtime);
|