@indigoai-us/hq-cloud 6.18.49-alpha.7 → 6.18.49
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/bin/sync-runner-company.d.ts +3 -5
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +13 -22
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +2 -12
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +112 -675
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +5 -27
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +15 -133
- package/dist/bin/sync-runner.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 +217 -31
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +0 -12
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +4 -57
- package/dist/cli/share.js.map +1 -1
- package/dist/lib/cloud-authoritative.d.ts +2 -9
- package/dist/lib/cloud-authoritative.d.ts.map +1 -1
- package/dist/lib/cloud-authoritative.js +2 -34
- package/dist/lib/cloud-authoritative.js.map +1 -1
- package/dist/outposts/session-host-command.d.ts +13 -0
- package/dist/outposts/session-host-command.d.ts.map +1 -1
- package/dist/outposts/session-host-command.js +34 -17
- package/dist/outposts/session-host-command.js.map +1 -1
- package/dist/outposts/session-host-connection.d.ts +2 -0
- package/dist/outposts/session-host-connection.d.ts.map +1 -1
- package/dist/outposts/session-host-connection.js +15 -0
- package/dist/outposts/session-host-connection.js.map +1 -1
- package/dist/outposts/session-host-server.d.ts +4 -0
- package/dist/outposts/session-host-server.d.ts.map +1 -1
- package/dist/outposts/session-host-server.js +42 -0
- package/dist/outposts/session-host-server.js.map +1 -1
- package/dist/session-log-capture.d.ts.map +1 -1
- package/dist/session-log-capture.js +140 -0
- package/dist/session-log-capture.js.map +1 -1
- package/dist/symlink-target-policy.d.ts +0 -11
- package/dist/symlink-target-policy.d.ts.map +1 -1
- package/dist/symlink-target-policy.js +0 -80
- package/dist/symlink-target-policy.js.map +1 -1
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +12 -0
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.d.ts +0 -4
- package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
- package/dist/sync/parallel-sync-coordinator.js +14 -155
- package/dist/sync/parallel-sync-coordinator.js.map +1 -1
- package/dist/sync/push-receiver.d.ts +20 -6
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +160 -34
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/watcher.d.ts +3 -3
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +7 -15
- package/dist/watcher.js.map +1 -1
- package/package.json +2 -1
- package/dist/bin/watcher-catchup-skip-store.d.ts +0 -59
- package/dist/bin/watcher-catchup-skip-store.d.ts.map +0 -1
- package/dist/bin/watcher-catchup-skip-store.js +0 -278
- package/dist/bin/watcher-catchup-skip-store.js.map +0 -1
package/dist/cli/rescue-core.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { spawnSync } from "child_process";
|
|
31
31
|
import * as crypto from "crypto";
|
|
32
32
|
import * as fs from "fs";
|
|
33
|
+
import { createRequire } from "node:module";
|
|
33
34
|
import * as os from "os";
|
|
34
35
|
import * as path from "path";
|
|
35
36
|
import yaml from "js-yaml";
|
|
@@ -37,6 +38,40 @@ import { assertRescuePathTypeUnchanged, isRescueTransientDirectory, } from "./re
|
|
|
37
38
|
import { applyBackupPrune, canonicalRootHash, inventoryManagedSnapshots, isManagedSnapshotManifest, } from "../backup-prune.js";
|
|
38
39
|
import { makeRestoreProbePath, makeRestoreStagePath, } from "../restore-staging.js";
|
|
39
40
|
import { readCoreSourceStamp, writeCoreSourceStamp } from "./core-source-stamp.js";
|
|
41
|
+
const MAX_CLONE_FILES_PER_BATCH = 200;
|
|
42
|
+
const MAX_CLONE_BATCH_PATH_BYTES = 64 * 1024;
|
|
43
|
+
const require = createRequire(import.meta.url);
|
|
44
|
+
function runDarwinClone(sources, destinationDirectory, destinationPaths) {
|
|
45
|
+
// Node/libuv does not implement forced APFS clones on macOS. Use the native
|
|
46
|
+
// clonefile(2) binding, which fails when the volume cannot clone instead of
|
|
47
|
+
// silently consuming the full-copy budget.
|
|
48
|
+
const cloneFileSync = require("rclonefile").cloneFileSync;
|
|
49
|
+
for (const [index, source] of sources.entries()) {
|
|
50
|
+
const destination = destinationPaths?.[index] ??
|
|
51
|
+
path.join(destinationDirectory, path.basename(source));
|
|
52
|
+
cloneFileSync(source, destination);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function chunkCloneSources(sources, destinationDirectory) {
|
|
56
|
+
const batches = [];
|
|
57
|
+
let batch = [];
|
|
58
|
+
let batchPathBytes = Buffer.byteLength(destinationDirectory) + 8;
|
|
59
|
+
for (const source of sources) {
|
|
60
|
+
const nextBytes = Buffer.byteLength(source) + 1;
|
|
61
|
+
if (batch.length > 0 &&
|
|
62
|
+
(batch.length >= MAX_CLONE_FILES_PER_BATCH ||
|
|
63
|
+
batchPathBytes + nextBytes > MAX_CLONE_BATCH_PATH_BYTES)) {
|
|
64
|
+
batches.push(batch);
|
|
65
|
+
batch = [];
|
|
66
|
+
batchPathBytes = Buffer.byteLength(destinationDirectory) + 8;
|
|
67
|
+
}
|
|
68
|
+
batch.push(source);
|
|
69
|
+
batchPathBytes += nextBytes;
|
|
70
|
+
}
|
|
71
|
+
if (batch.length > 0)
|
|
72
|
+
batches.push(batch);
|
|
73
|
+
return batches;
|
|
74
|
+
}
|
|
40
75
|
// Paths always preserved across wipe+overlay (shuttled out, restored after).
|
|
41
76
|
const CARVE_OUT_PATHS = [
|
|
42
77
|
"core/packages",
|
|
@@ -733,7 +768,7 @@ export function runRescue(argv, opts = {}) {
|
|
|
733
768
|
tmpdir = d;
|
|
734
769
|
}, () => {
|
|
735
770
|
retainTmpdir = true;
|
|
736
|
-
}, opts.gitReachabilityProbeTimeoutMs ?? GIT_REACHABILITY_PROBE_TIMEOUT_MS, opts.coreSourceStampWriter, opts.restoreOnRsyncFailure === true, opts.setupPathSettingsLocal === true, opts.backupCapacityStatfsSync ?? fs.statfsSync, opts.backupCapacityCloneProbe
|
|
771
|
+
}, opts.gitReachabilityProbeTimeoutMs ?? GIT_REACHABILITY_PROBE_TIMEOUT_MS, opts.coreSourceStampWriter, opts.restoreOnRsyncFailure === true, opts.setupPathSettingsLocal === true, opts.backupCapacityStatfsSync ?? fs.statfsSync, opts.backupCapacityCloneProbe, opts.backupCapacityStatSync ?? fs.statSync, opts.pathSepForTest ?? path.sep, opts.snapshotClonePlatform ?? process.platform, opts.snapshotCloneRunner ?? runDarwinClone);
|
|
737
772
|
}
|
|
738
773
|
catch (e) {
|
|
739
774
|
if (e instanceof ExitError)
|
|
@@ -1536,7 +1571,7 @@ function recoveryProof(unresolved, cfg, hqRoot, srcDir, srcSha, rsyncLauncher, r
|
|
|
1536
1571
|
}
|
|
1537
1572
|
return { eligible: !hasSkippedSnapshotEntries(newest.entry.manifest), failures };
|
|
1538
1573
|
}
|
|
1539
|
-
function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTimeoutMs = GIT_REACHABILITY_PROBE_TIMEOUT_MS, stampWriter = writeCoreSourceStamp, restoreOnRsyncFailure = false, setupPathSettingsLocal = false, backupCapacityStatfsSync = fs.statfsSync, backupCapacityCloneProbe
|
|
1574
|
+
function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTimeoutMs = GIT_REACHABILITY_PROBE_TIMEOUT_MS, stampWriter = writeCoreSourceStamp, restoreOnRsyncFailure = false, setupPathSettingsLocal = false, backupCapacityStatfsSync = fs.statfsSync, backupCapacityCloneProbe, backupCapacityStatSync = fs.statSync, pathSep = path.sep, snapshotClonePlatform = process.platform, snapshotCloneRunner = runDarwinClone) {
|
|
1540
1575
|
const hqRoot = resolveHqRoot(cfg, err);
|
|
1541
1576
|
const RUN_TS = utcStamp(new Date(), "dash");
|
|
1542
1577
|
// Both the release overlay and a restorable snapshot require rsync. A shim
|
|
@@ -2052,7 +2087,7 @@ function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTim
|
|
|
2052
2087
|
}
|
|
2053
2088
|
}
|
|
2054
2089
|
}
|
|
2055
|
-
const snapshotCapacityAvailable = requireBackupCapacity(cfg, hqRoot, snapshotRoots, srcDir, env, err, backupCapacityStatfsSync, backupCapacityCloneProbe, backupCapacityStatSync);
|
|
2090
|
+
const snapshotCapacityAvailable = requireBackupCapacity(cfg, hqRoot, snapshotRoots, srcDir, env, err, backupCapacityStatfsSync, backupCapacityCloneProbe, backupCapacityStatSync, snapshotClonePlatform, snapshotCloneRunner);
|
|
2056
2091
|
if (!snapshotCapacityAvailable) {
|
|
2057
2092
|
cfg.doBackup = false;
|
|
2058
2093
|
out("\nwarning: safety snapshot skipped because available space cannot cover the snapshot allowance; the cloud update continues without a pre-update restore point.\n");
|
|
@@ -2076,7 +2111,11 @@ function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTim
|
|
|
2076
2111
|
if (isRescueTransientDirectory(rootRel))
|
|
2077
2112
|
continue;
|
|
2078
2113
|
try {
|
|
2079
|
-
const copy = cpAToFiltered(srcAbs, path.join(backupDir, rootRel), rootRel, hqRoot, snapshotSymlinks, snapshotSkippedErrors
|
|
2114
|
+
const copy = cpAToFiltered(srcAbs, path.join(backupDir, rootRel), rootRel, hqRoot, snapshotSymlinks, snapshotSkippedErrors, {
|
|
2115
|
+
platform: snapshotClonePlatform,
|
|
2116
|
+
runner: snapshotCloneRunner,
|
|
2117
|
+
statfsSync: backupCapacityStatfsSync,
|
|
2118
|
+
});
|
|
2080
2119
|
if (!copy.captured) {
|
|
2081
2120
|
const failure = copy.firstFailure ??
|
|
2082
2121
|
new SnapshotCopyError(rootRel, "metadata", new Error("snapshot root contains no capturable entries"), srcAbs);
|
|
@@ -2188,7 +2227,11 @@ function doRescue(cfg, env, out, err, setTmp, retainTmp, gitReachabilityProbeTim
|
|
|
2188
2227
|
if (lexists(src)) {
|
|
2189
2228
|
shuttleId += 1;
|
|
2190
2229
|
try {
|
|
2191
|
-
cpA(src, shuttle, String(shuttleId)
|
|
2230
|
+
cpA(src, shuttle, String(shuttleId), {
|
|
2231
|
+
platform: snapshotClonePlatform,
|
|
2232
|
+
runner: snapshotCloneRunner,
|
|
2233
|
+
statfsSync: backupCapacityStatfsSync,
|
|
2234
|
+
});
|
|
2192
2235
|
}
|
|
2193
2236
|
catch (error) {
|
|
2194
2237
|
reportPreserveBackupFailure(normalizedSp, error, err);
|
|
@@ -4650,6 +4693,10 @@ function estimateFiltered(abs, rel) {
|
|
|
4650
4693
|
}
|
|
4651
4694
|
return { files, bytes };
|
|
4652
4695
|
}
|
|
4696
|
+
function snapshotFreeSpaceReserve(fsStat) {
|
|
4697
|
+
const capacity = Number(fsStat.blocks) * Number(fsStat.bsize);
|
|
4698
|
+
return Math.min(10 * 1024 ** 3, Math.max(5 * 1024 ** 3, Math.ceil(capacity * 0.1)));
|
|
4699
|
+
}
|
|
4653
4700
|
function estimateShuttleBytes(abs) {
|
|
4654
4701
|
const st = lstatOrNull(abs);
|
|
4655
4702
|
if (!st)
|
|
@@ -4678,8 +4725,9 @@ function estimateShuttleBytes(abs) {
|
|
|
4678
4725
|
.readdirSync(abs)
|
|
4679
4726
|
.reduce((total, name) => total + estimateShuttleBytes(path.join(abs, name)), 0);
|
|
4680
4727
|
}
|
|
4681
|
-
export function requireBackupCapacity(cfg, hqRoot, roots, srcDir, _env, err, statfsSync = fs.statfsSync, cloneProbe =
|
|
4728
|
+
export function requireBackupCapacity(cfg, hqRoot, roots, srcDir, _env, err, statfsSync = fs.statfsSync, cloneProbe = undefined, statSync = fs.statSync, platform = process.platform, cloneRunner = runDarwinClone) {
|
|
4682
4729
|
try {
|
|
4730
|
+
const probe = cloneProbe ?? ((root, backup, capturedRoots) => probeCopyOnWriteClone(root, backup, capturedRoots, platform, cloneRunner, statSync));
|
|
4683
4731
|
const estimate = roots.reduce((total, root) => total + estimateFiltered(path.join(hqRoot, root), root).bytes, 0);
|
|
4684
4732
|
const backupLocation = nearestExistingDirectory(cfg.backupRoot);
|
|
4685
4733
|
const tempLocation = nearestExistingDirectory(os.tmpdir());
|
|
@@ -4688,18 +4736,17 @@ export function requireBackupCapacity(cfg, hqRoot, roots, srcDir, _env, err, sta
|
|
|
4688
4736
|
const tempDevice = Number(statSync(tempLocation).dev);
|
|
4689
4737
|
const backupFs = statfsSync(backupLocation);
|
|
4690
4738
|
const backupFree = Number(backupFs.bavail) * Number(backupFs.bsize);
|
|
4691
|
-
const backupCapacity = Number(backupFs.blocks) * Number(backupFs.bsize);
|
|
4692
4739
|
const sameHqBackupFilesystem = hqDevice === backupDevice;
|
|
4693
4740
|
const sameBackupTempFilesystem = backupDevice === tempDevice;
|
|
4694
4741
|
const sameHqTempFilesystem = hqDevice === tempDevice;
|
|
4695
4742
|
const shuttleBytes = (cfg.preserveSubpaths ?? []).reduce((total, rel) => total + estimateShuttleBytes(path.join(hqRoot, rel)), 0);
|
|
4696
|
-
const snapshotCloneSupported = sameHqBackupFilesystem &&
|
|
4743
|
+
const snapshotCloneSupported = sameHqBackupFilesystem && probe(hqRoot, cfg.backupRoot, roots);
|
|
4697
4744
|
const shuttleCloneSupported = shuttleBytes > 0 &&
|
|
4698
4745
|
sameHqTempFilesystem &&
|
|
4699
4746
|
(sameBackupTempFilesystem && snapshotCloneSupported
|
|
4700
4747
|
? true
|
|
4701
|
-
:
|
|
4702
|
-
const backupMargin =
|
|
4748
|
+
: probe(hqRoot, tempLocation, cfg.preserveSubpaths ?? []));
|
|
4749
|
+
const backupMargin = snapshotFreeSpaceReserve(backupFs);
|
|
4703
4750
|
const snapshotRequired = snapshotCloneSupported
|
|
4704
4751
|
? SNAPSHOT_CLONE_METADATA_ALLOWANCE_BYTES
|
|
4705
4752
|
: estimate + backupMargin;
|
|
@@ -4716,8 +4763,7 @@ export function requireBackupCapacity(cfg, hqRoot, roots, srcDir, _env, err, sta
|
|
|
4716
4763
|
if (!sameBackupTempFilesystem && shuttleBytes > 0) {
|
|
4717
4764
|
const tempFs = statfsSync(tempLocation);
|
|
4718
4765
|
shuttleFree = Number(tempFs.bavail) * Number(tempFs.bsize);
|
|
4719
|
-
const
|
|
4720
|
-
const tempMargin = Math.min(10 * 1024 ** 3, Math.max(5 * 1024 ** 3, Math.ceil(tempCapacity * 0.1)));
|
|
4766
|
+
const tempMargin = snapshotFreeSpaceReserve(tempFs);
|
|
4721
4767
|
shuttleRequired = shuttleCloneSupported
|
|
4722
4768
|
? SNAPSHOT_CLONE_METADATA_ALLOWANCE_BYTES
|
|
4723
4769
|
: shuttleBytes + tempMargin;
|
|
@@ -4760,12 +4806,7 @@ export function requireBackupCapacity(cfg, hqRoot, roots, srcDir, _env, err, sta
|
|
|
4760
4806
|
}
|
|
4761
4807
|
return true;
|
|
4762
4808
|
}
|
|
4763
|
-
function probeCopyOnWriteClone(hqRoot, backupRoot, roots) {
|
|
4764
|
-
const source = roots
|
|
4765
|
-
.map((root) => firstRegularFile(path.join(hqRoot, root)))
|
|
4766
|
-
.find((candidate) => candidate !== null);
|
|
4767
|
-
if (!source)
|
|
4768
|
-
return false;
|
|
4809
|
+
function probeCopyOnWriteClone(hqRoot, backupRoot, roots, platform = process.platform, cloneRunner = runDarwinClone, statSync = fs.statSync) {
|
|
4769
4810
|
const parent = nearestExistingDirectory(backupRoot);
|
|
4770
4811
|
let probeDir;
|
|
4771
4812
|
try {
|
|
@@ -4777,7 +4818,33 @@ function probeCopyOnWriteClone(hqRoot, backupRoot, roots) {
|
|
|
4777
4818
|
throw error;
|
|
4778
4819
|
}
|
|
4779
4820
|
try {
|
|
4780
|
-
|
|
4821
|
+
if (platform === "darwin") {
|
|
4822
|
+
const hqDevice = Number(statSync(hqRoot).dev);
|
|
4823
|
+
for (let index = 0; index < roots.length; index += 1) {
|
|
4824
|
+
const sample = darwinCloneSampleOnDevice(path.join(hqRoot, roots[index]), hqDevice, statSync);
|
|
4825
|
+
if (!sample.sameDevice)
|
|
4826
|
+
return false;
|
|
4827
|
+
if (!sample.file)
|
|
4828
|
+
continue;
|
|
4829
|
+
const destination = path.join(probeDir, `root-${index}`);
|
|
4830
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
4831
|
+
try {
|
|
4832
|
+
cloneRunner([sample.file], destination);
|
|
4833
|
+
}
|
|
4834
|
+
catch {
|
|
4835
|
+
return false;
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
return true;
|
|
4839
|
+
}
|
|
4840
|
+
else {
|
|
4841
|
+
const source = roots
|
|
4842
|
+
.map((root) => firstRegularFile(path.join(hqRoot, root)))
|
|
4843
|
+
.find((candidate) => candidate !== null);
|
|
4844
|
+
if (!source)
|
|
4845
|
+
return false;
|
|
4846
|
+
fs.copyFileSync(source, path.join(probeDir, "probe"), fs.constants.COPYFILE_FICLONE_FORCE);
|
|
4847
|
+
}
|
|
4781
4848
|
return true;
|
|
4782
4849
|
}
|
|
4783
4850
|
catch (error) {
|
|
@@ -4798,6 +4865,33 @@ function probeCopyOnWriteClone(hqRoot, backupRoot, roots) {
|
|
|
4798
4865
|
fs.rmSync(probeDir, { recursive: true, force: true });
|
|
4799
4866
|
}
|
|
4800
4867
|
}
|
|
4868
|
+
function darwinCloneSampleOnDevice(root, hqDevice, statSync) {
|
|
4869
|
+
let sample;
|
|
4870
|
+
const walk = (absolute, relative) => {
|
|
4871
|
+
const lstat = lstatOrNull(absolute);
|
|
4872
|
+
if (!lstat || lstat.isSymbolicLink())
|
|
4873
|
+
return true;
|
|
4874
|
+
if (relative && isRescueTransientDirectory(relative))
|
|
4875
|
+
return true;
|
|
4876
|
+
if (Number(statSync(absolute).dev) !== hqDevice)
|
|
4877
|
+
return false;
|
|
4878
|
+
if (lstat.isFile()) {
|
|
4879
|
+
sample ??= absolute;
|
|
4880
|
+
return true;
|
|
4881
|
+
}
|
|
4882
|
+
if (!lstat.isDirectory())
|
|
4883
|
+
return true;
|
|
4884
|
+
for (const name of fs.readdirSync(absolute)) {
|
|
4885
|
+
const childRelative = path.posix.join(relative, name);
|
|
4886
|
+
if (isRescueTransientDirectory(childRelative))
|
|
4887
|
+
continue;
|
|
4888
|
+
if (!walk(path.join(absolute, name), childRelative))
|
|
4889
|
+
return false;
|
|
4890
|
+
}
|
|
4891
|
+
return true;
|
|
4892
|
+
};
|
|
4893
|
+
return walk(root, "") ? { sameDevice: true, ...(sample ? { file: sample } : {}) } : { sameDevice: false };
|
|
4894
|
+
}
|
|
4801
4895
|
function firstRegularFile(root, rel = "") {
|
|
4802
4896
|
const st = lstatOrNull(root);
|
|
4803
4897
|
if (!st || st.isSymbolicLink())
|
|
@@ -4871,11 +4965,11 @@ function countFiles(dir) {
|
|
|
4871
4965
|
* source basename into destDir) and the shuttle form (`cp -a "$src"
|
|
4872
4966
|
* "$SHUTTLE/$id"`, an explicit destination name).
|
|
4873
4967
|
*/
|
|
4874
|
-
function cpA(src, destDir, asName) {
|
|
4968
|
+
function cpA(src, destDir, asName, cloneOptions) {
|
|
4875
4969
|
const dest = asName
|
|
4876
4970
|
? path.join(destDir, asName)
|
|
4877
4971
|
: path.join(destDir, path.basename(src));
|
|
4878
|
-
cpATo(src, dest);
|
|
4972
|
+
cpATo(src, dest, cloneOptions);
|
|
4879
4973
|
}
|
|
4880
4974
|
/** Copy a link, using the same Windows representation choices as snapshots. */
|
|
4881
4975
|
function copySymlinkWithWindowsFallback(src, dest, link) {
|
|
@@ -4902,7 +4996,7 @@ function copySymlinkWithWindowsFallback(src, dest, link) {
|
|
|
4902
4996
|
throw linkPrivilegeError;
|
|
4903
4997
|
}
|
|
4904
4998
|
/** `cp -a <src> <dest>` with an explicit destination path. */
|
|
4905
|
-
function cpATo(src, dest) {
|
|
4999
|
+
function cpATo(src, dest, cloneOptions) {
|
|
4906
5000
|
const st = lstatOrNull(src);
|
|
4907
5001
|
if (!st)
|
|
4908
5002
|
return;
|
|
@@ -4920,7 +5014,7 @@ function cpATo(src, dest) {
|
|
|
4920
5014
|
}
|
|
4921
5015
|
fs.mkdirSync(dest, { recursive: true });
|
|
4922
5016
|
for (const name of fs.readdirSync(src)) {
|
|
4923
|
-
cpATo(path.join(src, name), path.join(dest, name));
|
|
5017
|
+
cpATo(path.join(src, name), path.join(dest, name), cloneOptions);
|
|
4924
5018
|
}
|
|
4925
5019
|
try {
|
|
4926
5020
|
fs.utimesSync(dest, st.atime, st.mtime);
|
|
@@ -4935,10 +5029,13 @@ function cpATo(src, dest) {
|
|
|
4935
5029
|
if (destinationStats && !destinationStats.isFile()) {
|
|
4936
5030
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
4937
5031
|
}
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
5032
|
+
if (cloneOptions) {
|
|
5033
|
+
copyRegularFileForSnapshot(src, dest, path.basename(src), cloneOptions);
|
|
5034
|
+
}
|
|
5035
|
+
else {
|
|
5036
|
+
// Restore copies remain independent files and retain their existing path.
|
|
5037
|
+
fs.copyFileSync(src, dest, fs.constants.COPYFILE_FICLONE);
|
|
5038
|
+
}
|
|
4942
5039
|
try {
|
|
4943
5040
|
fs.chmodSync(dest, st.mode);
|
|
4944
5041
|
fs.utimesSync(dest, st.atime, st.mtime);
|
|
@@ -5389,7 +5486,25 @@ function validateSnapshotSymlink(hqRoot, rel, target) {
|
|
|
5389
5486
|
? null
|
|
5390
5487
|
: `target for ${rel} resolves outside the HQ root`;
|
|
5391
5488
|
}
|
|
5392
|
-
function copyRegularFileForSnapshot(src, dest, rel) {
|
|
5489
|
+
function copyRegularFileForSnapshot(src, dest, rel, cloneOptions) {
|
|
5490
|
+
if (cloneOptions?.platform === "darwin") {
|
|
5491
|
+
try {
|
|
5492
|
+
cloneOptions.runner([src], path.dirname(dest), [dest]);
|
|
5493
|
+
return;
|
|
5494
|
+
}
|
|
5495
|
+
catch {
|
|
5496
|
+
try {
|
|
5497
|
+
if (lstatOrNull(dest))
|
|
5498
|
+
fs.rmSync(dest, { force: true });
|
|
5499
|
+
assertFullCopySnapshotCapacity(src, dest, rel, cloneOptions.statfsSync);
|
|
5500
|
+
fs.copyFileSync(src, dest);
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
catch (fallbackError) {
|
|
5504
|
+
throw classifySnapshotCopyFailure(rel, src, dest, fallbackError);
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5393
5508
|
try {
|
|
5394
5509
|
// Node falls back to a full copy when the filesystem cannot reflink.
|
|
5395
5510
|
// Restore paths use ordinary copies, so they remain independent files.
|
|
@@ -5408,6 +5523,23 @@ function copyRegularFileForSnapshot(src, dest, rel) {
|
|
|
5408
5523
|
throw classifySnapshotCopyFailure(rel, src, dest, error);
|
|
5409
5524
|
}
|
|
5410
5525
|
}
|
|
5526
|
+
function assertFullCopySnapshotCapacity(src, dest, rel, statfsSync) {
|
|
5527
|
+
try {
|
|
5528
|
+
const bytes = fs.statSync(src).size;
|
|
5529
|
+
const location = nearestExistingDirectory(path.dirname(dest));
|
|
5530
|
+
const fsStat = statfsSync(location);
|
|
5531
|
+
const available = Number(fsStat.bavail) * Number(fsStat.bsize);
|
|
5532
|
+
const required = bytes + snapshotFreeSpaceReserve(fsStat);
|
|
5533
|
+
if (available < required) {
|
|
5534
|
+
const error = new Error(`insufficient free space before full-copy snapshot fallback (need ${required} bytes, have ${available})`);
|
|
5535
|
+
error.code = "ENOSPC";
|
|
5536
|
+
throw error;
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
catch (error) {
|
|
5540
|
+
throw classifySnapshotCopyFailure(rel, src, dest, error);
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5411
5543
|
function classifySnapshotCopyFailure(rel, src, dest, error) {
|
|
5412
5544
|
if (error.code === "EDEADLK") {
|
|
5413
5545
|
return new SnapshotCopyError(rel, "read", error, src);
|
|
@@ -5443,7 +5575,7 @@ function copySymlinkFileRepresentationForSnapshot(src, dest, rel) {
|
|
|
5443
5575
|
preserveSnapshotMetadata(dest, targetStats);
|
|
5444
5576
|
}
|
|
5445
5577
|
/** Snapshot copy that preserves metadata while pruning canonical transient directories. */
|
|
5446
|
-
function cpAToFiltered(src, dest, rel, hqRoot, symlinks, skippedEntries) {
|
|
5578
|
+
function cpAToFiltered(src, dest, rel, hqRoot, symlinks, skippedEntries, cloneOptions) {
|
|
5447
5579
|
let st;
|
|
5448
5580
|
try {
|
|
5449
5581
|
st = fs.lstatSync(src);
|
|
@@ -5489,9 +5621,63 @@ function cpAToFiltered(src, dest, rel, hqRoot, symlinks, skippedEntries) {
|
|
|
5489
5621
|
}
|
|
5490
5622
|
let captured = names.length === 0;
|
|
5491
5623
|
let firstFailure;
|
|
5624
|
+
const batchClonedNames = new Set();
|
|
5625
|
+
if (cloneOptions.platform === "darwin") {
|
|
5626
|
+
const regularNames = names.filter((name) => {
|
|
5627
|
+
try {
|
|
5628
|
+
const child = lstatOrNull(path.join(src, name));
|
|
5629
|
+
return Boolean(child && !child.isSymbolicLink() && child.isFile());
|
|
5630
|
+
}
|
|
5631
|
+
catch {
|
|
5632
|
+
return false;
|
|
5633
|
+
}
|
|
5634
|
+
});
|
|
5635
|
+
if (regularNames.length > 0) {
|
|
5636
|
+
const sources = regularNames.map((name) => path.join(src, name));
|
|
5637
|
+
for (const batch of chunkCloneSources(sources, dest)) {
|
|
5638
|
+
const destinations = batch.map((source) => path.join(dest, path.basename(source)));
|
|
5639
|
+
let batchCloneFailed = false;
|
|
5640
|
+
try {
|
|
5641
|
+
cloneOptions.runner(batch, dest, destinations);
|
|
5642
|
+
}
|
|
5643
|
+
catch {
|
|
5644
|
+
batchCloneFailed = true;
|
|
5645
|
+
for (const destination of destinations) {
|
|
5646
|
+
fs.rmSync(destination, { force: true });
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
for (const [index, source] of batch.entries()) {
|
|
5650
|
+
const destination = destinations[index];
|
|
5651
|
+
const name = path.basename(source);
|
|
5652
|
+
const relative = path.posix.join(rel, name);
|
|
5653
|
+
try {
|
|
5654
|
+
if (batchCloneFailed) {
|
|
5655
|
+
copyRegularFileForSnapshot(source, destination, relative, cloneOptions);
|
|
5656
|
+
}
|
|
5657
|
+
const sourceStats = fs.lstatSync(source);
|
|
5658
|
+
preserveSnapshotMetadata(destination, sourceStats);
|
|
5659
|
+
captured = true;
|
|
5660
|
+
}
|
|
5661
|
+
catch (error) {
|
|
5662
|
+
if (!(error instanceof SnapshotCopyError))
|
|
5663
|
+
throw error;
|
|
5664
|
+
if (error.code === "ENOSPC")
|
|
5665
|
+
throw error;
|
|
5666
|
+
skippedEntries.push(error);
|
|
5667
|
+
firstFailure ??= error;
|
|
5668
|
+
}
|
|
5669
|
+
finally {
|
|
5670
|
+
batchClonedNames.add(name);
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5492
5676
|
for (const name of names) {
|
|
5677
|
+
if (batchClonedNames.has(name))
|
|
5678
|
+
continue;
|
|
5493
5679
|
try {
|
|
5494
|
-
const child = cpAToFiltered(path.join(src, name), path.join(dest, name), path.posix.join(rel, name), hqRoot, symlinks, skippedEntries);
|
|
5680
|
+
const child = cpAToFiltered(path.join(src, name), path.join(dest, name), path.posix.join(rel, name), hqRoot, symlinks, skippedEntries, cloneOptions);
|
|
5495
5681
|
captured ||= child.captured;
|
|
5496
5682
|
firstFailure ??= child.firstFailure;
|
|
5497
5683
|
}
|
|
@@ -5524,7 +5710,7 @@ function cpAToFiltered(src, dest, rel, hqRoot, symlinks, skippedEntries) {
|
|
|
5524
5710
|
catch (error) {
|
|
5525
5711
|
throw new SnapshotCopyError(rel, "write", error, dest);
|
|
5526
5712
|
}
|
|
5527
|
-
copyRegularFileForSnapshot(src, dest, rel);
|
|
5713
|
+
copyRegularFileForSnapshot(src, dest, rel, cloneOptions);
|
|
5528
5714
|
try {
|
|
5529
5715
|
fs.chmodSync(dest, st.mode);
|
|
5530
5716
|
fs.utimesSync(dest, st.atime, st.mtime);
|