@indigoai-us/hq-cloud 6.14.19 → 6.14.21
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-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +8 -16
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +138 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +14 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +55 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +15 -1
- package/dist/cli/sync.js.map +1 -1
- package/dist/s3.d.ts +23 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +51 -1
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +38 -1
- package/dist/s3.test.js.map +1 -1
- package/package.json +2 -2
- package/src/bin/sync-runner-planning.test.ts +153 -0
- package/src/bin/sync-runner-planning.ts +10 -17
- package/src/bin/sync-runner.test.ts +71 -0
- package/src/bin/sync-runner.ts +15 -2
- package/src/cli/sync.ts +14 -0
- package/src/s3.test.ts +45 -0
- package/src/s3.ts +54 -1
package/src/s3.test.ts
CHANGED
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
createStagedSymlink,
|
|
99
99
|
replaceStagedPath,
|
|
100
100
|
sweepStaleStagedFiles,
|
|
101
|
+
DanglingSymlinkParentError,
|
|
101
102
|
} from "./s3.js";
|
|
102
103
|
import {
|
|
103
104
|
setObjectIOFactory,
|
|
@@ -841,6 +842,50 @@ describe("downloadFile", () => {
|
|
|
841
842
|
expect(fs.readlinkSync(localPath)).toBe(target);
|
|
842
843
|
});
|
|
843
844
|
|
|
845
|
+
it("skips (typed, non-fatal) when the parent directory is a DANGLING symlink", async () => {
|
|
846
|
+
// The vault stores directory symlinks as objects (e.g.
|
|
847
|
+
// companies/<co>/.obsidian -> ../../.obsidian) AND files beneath them. On a
|
|
848
|
+
// machine where the link target is absent, mkdir of the parent throws
|
|
849
|
+
// ENOENT — not EEXIST — because mkdir(2) returns EEXIST for the link and
|
|
850
|
+
// Node's recursive impl then stats it, which fails on a dangling link.
|
|
851
|
+
// Raw, that marked the WHOLE company errored/partial on every sync cycle
|
|
852
|
+
// (2026-07-24 dogfood box). It must be a typed per-object skip instead.
|
|
853
|
+
nextGetObjectResponse = {
|
|
854
|
+
Body: (async function* () {
|
|
855
|
+
yield new TextEncoder().encode("hotkeys");
|
|
856
|
+
})(),
|
|
857
|
+
Metadata: {},
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const parent = path.join(tmpRoot, ".obsidian");
|
|
861
|
+
fs.symlinkSync(path.join(tmpRoot, "does-not-exist"), parent);
|
|
862
|
+
// Precondition: existsSync follows the link and reports absent, while
|
|
863
|
+
// lstat still sees it — the exact shape the fix keys on.
|
|
864
|
+
expect(fs.existsSync(parent)).toBe(false);
|
|
865
|
+
expect(fs.lstatSync(parent).isSymbolicLink()).toBe(true);
|
|
866
|
+
|
|
867
|
+
await expect(
|
|
868
|
+
downloadFile(makeCtx(), ".obsidian/hotkeys.json", path.join(parent, "hotkeys.json")),
|
|
869
|
+
).rejects.toBeInstanceOf(DanglingSymlinkParentError);
|
|
870
|
+
|
|
871
|
+
// The link is left exactly as found — no target materialized outside the
|
|
872
|
+
// directory being synced, which the caller's containment guard refuses.
|
|
873
|
+
expect(fs.lstatSync(parent).isSymbolicLink()).toBe(true);
|
|
874
|
+
expect(fs.existsSync(path.join(tmpRoot, "does-not-exist"))).toBe(false);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("still creates an ordinary missing parent directory", async () => {
|
|
878
|
+
nextGetObjectResponse = {
|
|
879
|
+
Body: (async function* () {
|
|
880
|
+
yield new TextEncoder().encode("plain");
|
|
881
|
+
})(),
|
|
882
|
+
Metadata: {},
|
|
883
|
+
};
|
|
884
|
+
const localPath = path.join(tmpRoot, "deep", "nested", "file.md");
|
|
885
|
+
await downloadFile(makeCtx(), "deep/nested/file.md", localPath);
|
|
886
|
+
expect(fs.readFileSync(localPath, "utf8")).toBe("plain");
|
|
887
|
+
});
|
|
888
|
+
|
|
844
889
|
it("recovers the target from legacy metadata-only uploads (body lacks the prefix)", async () => {
|
|
845
890
|
// Backward-compat: an in-flight upload from earlier in this
|
|
846
891
|
// PR's lifetime stored the target in metadata (raw or base64'd)
|
package/src/s3.ts
CHANGED
|
@@ -1003,6 +1003,59 @@ export interface DownloadModeWarning {
|
|
|
1003
1003
|
fallback?: "preserved-local-mode" | "receiver-default";
|
|
1004
1004
|
}
|
|
1005
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* A download whose parent directory exists as a DANGLING symlink.
|
|
1008
|
+
*
|
|
1009
|
+
* The vault stores directory symlinks as first-class objects (e.g.
|
|
1010
|
+
* `companies/<co>/.obsidian -> ../../.obsidian`) AND stores files beneath them.
|
|
1011
|
+
* When the link's target is absent on this machine, the child write fails —
|
|
1012
|
+
* and it fails as ENOENT rather than EEXIST, because `mkdir(2)` returns EEXIST
|
|
1013
|
+
* for the link and Node's `recursive: true` implementation then stats it, which
|
|
1014
|
+
* fails on a dangling link. Raw, that surfaced as a hard error that marked the
|
|
1015
|
+
* WHOLE company `errored`/`partial` on every sync cycle (2026-07-24 dogfood box:
|
|
1016
|
+
* `.obsidian/hotkeys.json: ENOENT ... mkdir '.../companies/indigo/.obsidian'`).
|
|
1017
|
+
*
|
|
1018
|
+
* It is a per-object condition, not a company-level failure, so it is typed and
|
|
1019
|
+
* the pull loop skips the object loudly instead of failing the run. We do NOT
|
|
1020
|
+
* materialize the link target: the target is by definition outside the
|
|
1021
|
+
* directory being synced, and creating it would write through a path the
|
|
1022
|
+
* caller's containment guard deliberately refuses.
|
|
1023
|
+
*/
|
|
1024
|
+
export class DanglingSymlinkParentError extends Error {
|
|
1025
|
+
readonly key: string;
|
|
1026
|
+
readonly dir: string;
|
|
1027
|
+
constructor(key: string, dir: string) {
|
|
1028
|
+
super(
|
|
1029
|
+
`download skipped: parent directory is a dangling symlink (${dir}) — ` +
|
|
1030
|
+
`the vault stores it as a link whose target is absent locally`,
|
|
1031
|
+
);
|
|
1032
|
+
this.name = "DanglingSymlinkParentError";
|
|
1033
|
+
this.key = key;
|
|
1034
|
+
this.dir = dir;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Create the parent directory for a download, distinguishing the
|
|
1040
|
+
* dangling-symlink case from a genuine mkdir failure.
|
|
1041
|
+
*
|
|
1042
|
+
* Callers must have already established that `dir` does not resolve
|
|
1043
|
+
* (`fs.existsSync(dir) === false`); `existsSync` FOLLOWS symlinks, so a
|
|
1044
|
+
* dangling link reports absent here while `lstat` still sees the link.
|
|
1045
|
+
*/
|
|
1046
|
+
function ensureDownloadParentDir(dir: string, key: string): void {
|
|
1047
|
+
let link: fs.Stats | null = null;
|
|
1048
|
+
try {
|
|
1049
|
+
link = fs.lstatSync(dir);
|
|
1050
|
+
} catch {
|
|
1051
|
+
// Genuinely absent — the ordinary first-write path.
|
|
1052
|
+
}
|
|
1053
|
+
if (link?.isSymbolicLink()) {
|
|
1054
|
+
throw new DanglingSymlinkParentError(key, dir);
|
|
1055
|
+
}
|
|
1056
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1006
1059
|
export async function downloadFile(
|
|
1007
1060
|
ctx: EntityContext,
|
|
1008
1061
|
key: string,
|
|
@@ -1032,7 +1085,7 @@ export async function downloadFile(
|
|
|
1032
1085
|
|
|
1033
1086
|
const dir = path.dirname(localPath);
|
|
1034
1087
|
if (!fs.existsSync(dir)) {
|
|
1035
|
-
|
|
1088
|
+
ensureDownloadParentDir(dir, key);
|
|
1036
1089
|
}
|
|
1037
1090
|
|
|
1038
1091
|
// Symlink path: presence of SYMLINK_TARGET_META_KEY (any non-empty
|