@indigoai-us/hq-cloud 6.18.26 → 6.18.28
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/core-source-stamp.d.ts +0 -5
- package/dist/cli/core-source-stamp.d.ts.map +1 -1
- package/dist/cli/core-source-stamp.js +1 -1
- package/dist/cli/core-source-stamp.js.map +1 -1
- package/dist/cli/rescue-breaker.test.js +277 -3
- package/dist/cli/rescue-breaker.test.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +6 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +293 -138
- 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/session-host-auth.d.ts +3 -2
- package/dist/outposts/session-host-auth.d.ts.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/session-log-retention.d.ts +0 -10
- package/dist/session-log-retention.d.ts.map +1 -1
- package/dist/session-log-retention.js +1 -1
- package/dist/session-log-retention.js.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 &&
|
|
@@ -4498,23 +4518,173 @@ function estimateFiltered(abs, rel) {
|
|
|
4498
4518
|
}
|
|
4499
4519
|
return { files, bytes };
|
|
4500
4520
|
}
|
|
4501
|
-
function
|
|
4521
|
+
function estimateShuttleBytes(abs) {
|
|
4522
|
+
const st = lstatOrNull(abs);
|
|
4523
|
+
if (!st)
|
|
4524
|
+
return 0;
|
|
4525
|
+
if (st.isSymbolicLink()) {
|
|
4526
|
+
const linkBytes = Buffer.byteLength(fs.readlinkSync(abs));
|
|
4527
|
+
if (process.platform === "win32") {
|
|
4528
|
+
try {
|
|
4529
|
+
const target = fs.statSync(abs);
|
|
4530
|
+
if (target.isFile())
|
|
4531
|
+
return Math.max(linkBytes, target.size);
|
|
4532
|
+
}
|
|
4533
|
+
catch (error) {
|
|
4534
|
+
const code = error.code;
|
|
4535
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
4536
|
+
throw error;
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
return linkBytes;
|
|
4540
|
+
}
|
|
4541
|
+
if (st.isFile())
|
|
4542
|
+
return st.size;
|
|
4543
|
+
if (!st.isDirectory())
|
|
4544
|
+
return 0;
|
|
4545
|
+
return fs
|
|
4546
|
+
.readdirSync(abs)
|
|
4547
|
+
.reduce((total, name) => total + estimateShuttleBytes(path.join(abs, name)), 0);
|
|
4548
|
+
}
|
|
4549
|
+
export function requireBackupCapacity(cfg, hqRoot, roots, srcDir, _env, err, statfsSync = fs.statfsSync, cloneProbe = probeCopyOnWriteClone, statSync = fs.statSync) {
|
|
4502
4550
|
try {
|
|
4503
4551
|
const estimate = roots.reduce((total, root) => total + estimateFiltered(path.join(hqRoot, root), root).bytes, 0);
|
|
4504
|
-
const
|
|
4505
|
-
const
|
|
4506
|
-
const
|
|
4507
|
-
const
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
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);
|
|
4558
|
+
const backupFree = Number(backupFs.bavail) * Number(backupFs.bsize);
|
|
4559
|
+
const backupCapacity = Number(backupFs.blocks) * Number(backupFs.bsize);
|
|
4560
|
+
const sameHqBackupFilesystem = hqDevice === backupDevice;
|
|
4561
|
+
const sameBackupTempFilesystem = backupDevice === tempDevice;
|
|
4562
|
+
const sameHqTempFilesystem = hqDevice === tempDevice;
|
|
4563
|
+
const shuttleBytes = (cfg.preserveSubpaths ?? []).reduce((total, rel) => total + estimateShuttleBytes(path.join(hqRoot, rel)), 0);
|
|
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);
|
|
4587
|
+
const tempCapacity = Number(tempFs.blocks) * Number(tempFs.bsize);
|
|
4588
|
+
const tempMargin = Math.min(10 * 1024 ** 3, Math.max(5 * 1024 ** 3, Math.ceil(tempCapacity * 0.1)));
|
|
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;
|
|
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);
|
|
4617
|
+
}
|
|
4618
|
+
err(`error: insufficient free space for safety snapshot (need ${backupRequired} bytes, have ${backupFree}).\n`);
|
|
4511
4619
|
err("HQ_RESCUE_FAILURE_KIND=disk_full\n");
|
|
4620
|
+
throw new ExitError(1);
|
|
4512
4621
|
}
|
|
4513
4622
|
catch (error) {
|
|
4623
|
+
if (error instanceof ExitError)
|
|
4624
|
+
throw error;
|
|
4514
4625
|
err(`error: could not measure snapshot size/free space (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
4515
4626
|
err("HQ_RESCUE_FAILURE_KIND=backup-capacity-check-failed\n");
|
|
4627
|
+
throw new ExitError(1);
|
|
4628
|
+
}
|
|
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;
|
|
4516
4686
|
}
|
|
4517
|
-
|
|
4687
|
+
return null;
|
|
4518
4688
|
}
|
|
4519
4689
|
function nearestExistingDirectory(target) {
|
|
4520
4690
|
let current = path.resolve(target);
|
|
@@ -4593,7 +4763,7 @@ function copySymlinkWithWindowsFallback(src, dest, link) {
|
|
|
4593
4763
|
const targetStats = fs.statSync(src);
|
|
4594
4764
|
if (targetStats.isFile()) {
|
|
4595
4765
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
4596
|
-
fs.copyFileSync(src, dest);
|
|
4766
|
+
fs.copyFileSync(src, dest, fs.constants.COPYFILE_FICLONE);
|
|
4597
4767
|
preserveSnapshotMetadata(dest, targetStats);
|
|
4598
4768
|
return;
|
|
4599
4769
|
}
|
|
@@ -4633,7 +4803,10 @@ function cpATo(src, dest) {
|
|
|
4633
4803
|
if (destinationStats && !destinationStats.isFile()) {
|
|
4634
4804
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
4635
4805
|
}
|
|
4636
|
-
|
|
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);
|
|
4637
4810
|
try {
|
|
4638
4811
|
fs.chmodSync(dest, st.mode);
|
|
4639
4812
|
fs.utimesSync(dest, st.atime, st.mtime);
|
|
@@ -5084,55 +5257,37 @@ function validateSnapshotSymlink(hqRoot, rel, target) {
|
|
|
5084
5257
|
? null
|
|
5085
5258
|
: `target for ${rel} resolves outside the HQ root`;
|
|
5086
5259
|
}
|
|
5087
|
-
function copyRegularFileForSnapshot(src, dest, rel
|
|
5088
|
-
let sourceFd;
|
|
5089
|
-
let destinationFd;
|
|
5260
|
+
function copyRegularFileForSnapshot(src, dest, rel) {
|
|
5090
5261
|
try {
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
try {
|
|
5098
|
-
destinationFd = fs.openSync(dest, "w", mode);
|
|
5099
|
-
}
|
|
5100
|
-
catch (error) {
|
|
5101
|
-
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5102
|
-
}
|
|
5103
|
-
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
5104
|
-
for (;;) {
|
|
5105
|
-
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") {
|
|
5106
5268
|
try {
|
|
5107
|
-
|
|
5269
|
+
fs.copyFileSync(src, dest);
|
|
5270
|
+
return;
|
|
5108
5271
|
}
|
|
5109
|
-
catch (
|
|
5110
|
-
throw
|
|
5111
|
-
}
|
|
5112
|
-
if (read === 0)
|
|
5113
|
-
break;
|
|
5114
|
-
let offset = 0;
|
|
5115
|
-
while (offset < read) {
|
|
5116
|
-
let written;
|
|
5117
|
-
try {
|
|
5118
|
-
written = fs.writeSync(destinationFd, buffer, offset, read - offset);
|
|
5119
|
-
}
|
|
5120
|
-
catch (error) {
|
|
5121
|
-
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5122
|
-
}
|
|
5123
|
-
if (written === 0) {
|
|
5124
|
-
throw new SnapshotCopyError(rel, "write", new Error("write returned zero bytes"));
|
|
5125
|
-
}
|
|
5126
|
-
offset += written;
|
|
5272
|
+
catch (fallbackError) {
|
|
5273
|
+
throw classifySnapshotCopyFailure(rel, src, dest, fallbackError);
|
|
5127
5274
|
}
|
|
5128
5275
|
}
|
|
5276
|
+
throw classifySnapshotCopyFailure(rel, src, dest, error);
|
|
5129
5277
|
}
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
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);
|
|
5135
5289
|
}
|
|
5290
|
+
return new SnapshotCopyError(rel, "write", error, dest);
|
|
5136
5291
|
}
|
|
5137
5292
|
/** Preserve a Windows file link as ordinary file data when links are forbidden. */
|
|
5138
5293
|
function copySymlinkFileRepresentationForSnapshot(src, dest, rel) {
|
|
@@ -5152,7 +5307,7 @@ function copySymlinkFileRepresentationForSnapshot(src, dest, rel) {
|
|
|
5152
5307
|
catch (error) {
|
|
5153
5308
|
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5154
5309
|
}
|
|
5155
|
-
copyRegularFileForSnapshot(src, dest, rel
|
|
5310
|
+
copyRegularFileForSnapshot(src, dest, rel);
|
|
5156
5311
|
preserveSnapshotMetadata(dest, targetStats);
|
|
5157
5312
|
}
|
|
5158
5313
|
/** Snapshot copy that preserves metadata while pruning canonical transient directories. */
|
|
@@ -5237,7 +5392,7 @@ function cpAToFiltered(src, dest, rel, hqRoot, symlinks, skippedEntries) {
|
|
|
5237
5392
|
catch (error) {
|
|
5238
5393
|
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5239
5394
|
}
|
|
5240
|
-
copyRegularFileForSnapshot(src, dest, rel
|
|
5395
|
+
copyRegularFileForSnapshot(src, dest, rel);
|
|
5241
5396
|
try {
|
|
5242
5397
|
fs.chmodSync(dest, st.mode);
|
|
5243
5398
|
fs.utimesSync(dest, st.atime, st.mtime);
|