@remnic/core 9.69.97 → 9.69.99
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/access-admin-ops-surface.js +10 -10
- package/dist/access-authorization-probe.js +11 -11
- package/dist/access-boundary.js +10 -10
- package/dist/access-cli.js +29 -29
- package/dist/access-extraction-force-flush.js +10 -10
- package/dist/access-http-briefing.js +11 -11
- package/dist/access-http-query.js +10 -10
- package/dist/access-http.js +23 -23
- package/dist/access-identity-continuity-surface.js +10 -10
- package/dist/access-lcm-surface.js +10 -10
- package/dist/access-mcp.js +17 -17
- package/dist/access-namespace-preflight.js +10 -10
- package/dist/access-observe-write-category.js +7 -7
- package/dist/access-observe-write-surface.js +10 -10
- package/dist/access-offline-manifest.js +6 -6
- package/dist/access-operations-batch.js +11 -11
- package/dist/access-operations.js +16 -16
- package/dist/access-recall-concurrency.js +10 -10
- package/dist/access-recall-response.js +10 -10
- package/dist/access-recall-surface.js +10 -10
- package/dist/access-schema.js +6 -6
- package/dist/access-service.js +10 -10
- package/dist/access-surface-catalog.js +12 -12
- package/dist/active-recall.js +6 -6
- package/dist/{chunk-ETUZFFPU.js → chunk-3E6DGSGZ.js} +1 -1
- package/dist/chunk-3E6DGSGZ.js.map +1 -0
- package/dist/{chunk-UG5V3Z3R.js → chunk-3MK4OYYH.js} +3 -3
- package/dist/{chunk-MSAI3Q7D.js → chunk-3QH4NT72.js} +2 -2
- package/dist/{chunk-CTEUPJMV.js → chunk-42YP6FMP.js} +7 -7
- package/dist/{chunk-C6HPNPNK.js → chunk-53X6CL3A.js} +4 -4
- package/dist/{chunk-U36GYZ2P.js → chunk-55NZVUNI.js} +2 -2
- package/dist/{chunk-OEBUAC3O.js → chunk-76GQBIVX.js} +2 -2
- package/dist/{chunk-4NSLQ7EU.js → chunk-ANPRNTPT.js} +3 -3
- package/dist/{chunk-4NSLQ7EU.js.map → chunk-ANPRNTPT.js.map} +1 -1
- package/dist/{chunk-5S7SNRSJ.js → chunk-BCQU44UV.js} +2 -2
- package/dist/{chunk-ZQTYWSO3.js → chunk-GLJNGCCM.js} +2 -2
- package/dist/{chunk-SMLKHXPF.js → chunk-HGG63H3A.js} +2 -2
- package/dist/{chunk-P62DSJUK.js → chunk-IAKC3KEJ.js} +4 -4
- package/dist/{chunk-3BTHXB7Z.js → chunk-JWG43U5K.js} +2 -2
- package/dist/{chunk-IVEHY2VT.js → chunk-KJKTDMAD.js} +5 -5
- package/dist/chunk-KJKTDMAD.js.map +1 -0
- package/dist/{chunk-AXGJMDBS.js → chunk-KO4X4WRJ.js} +2 -2
- package/dist/{chunk-HJ3CBZIG.js → chunk-N4JZ7J2I.js} +7 -7
- package/dist/{chunk-VOZNBQNL.js → chunk-NHFWYPXN.js} +2 -2
- package/dist/{chunk-GXETU7AE.js → chunk-NI5MWLM3.js} +7 -7
- package/dist/{chunk-MZAH3CBT.js → chunk-NWBYJ4VH.js} +2 -2
- package/dist/{chunk-6BG364RN.js → chunk-ORVH5S7R.js} +2 -2
- package/dist/{chunk-ANGZWYL2.js → chunk-PGJYLYPK.js} +3 -3
- package/dist/{chunk-A3K2KKSG.js → chunk-PYYQCHJC.js} +2 -2
- package/dist/{chunk-WHIGRW7T.js → chunk-RA3OID2N.js} +2 -2
- package/dist/{chunk-RMV2JYOT.js → chunk-SZIJJGA2.js} +11 -11
- package/dist/{chunk-PH2V7EXZ.js → chunk-TK7G5ABN.js} +2 -2
- package/dist/{chunk-KYOZ5E6V.js → chunk-UEU5IURP.js} +2 -2
- package/dist/{chunk-NSKXRTVZ.js → chunk-UGEFYQWU.js} +2 -2
- package/dist/{chunk-AF5PW35O.js → chunk-XK6GP7QO.js} +2 -2
- package/dist/{chunk-YHZWVDP7.js → chunk-YB5GVH36.js} +2 -2
- package/dist/cli.js +27 -27
- package/dist/config.js +6 -6
- package/dist/deep-recall-http-glue.js +7 -7
- package/dist/external-wiki-access.js +11 -11
- package/dist/index.js +29 -29
- package/dist/memory-browse-operations.js +11 -11
- package/dist/offline-sync-changeset-generations.js +3 -3
- package/dist/offline-sync-embedding-generation.js +2 -2
- package/dist/offline-sync-exclude-globs.d.ts +1 -0
- package/dist/offline-sync-exclude-globs.d.ts.map +1 -1
- package/dist/offline-sync-exclude-globs.js +1 -1
- package/dist/offline-sync-snapshot-stream.js +6 -6
- package/dist/offline-sync-upload-staging.d.ts +2 -2
- package/dist/offline-sync-upload-staging.d.ts.map +1 -1
- package/dist/offline-sync-upload-staging.js +1 -1
- package/dist/offline-sync.d.ts.map +1 -1
- package/dist/offline-sync.js +5 -5
- package/dist/operator-toolkit.js +7 -7
- package/dist/orchestrator.js +29 -29
- package/dist/recall-navigation-operations.js +11 -11
- package/dist/recall-navigation.js +10 -10
- package/dist/resume-bundles.js +7 -7
- package/dist/support-passport/index.js +13 -13
- package/package.json +2 -2
- package/src/offline-sync-exclude-globs.ts +3 -2
- package/src/offline-sync-upload-staging.test.ts +324 -0
- package/src/offline-sync-upload-staging.ts +2 -1
- package/src/offline-sync.test.ts +12 -0
- package/src/offline-sync.ts +1 -6
- package/dist/chunk-ETUZFFPU.js.map +0 -1
- package/dist/chunk-IVEHY2VT.js.map +0 -1
- /package/dist/{chunk-UG5V3Z3R.js.map → chunk-3MK4OYYH.js.map} +0 -0
- /package/dist/{chunk-MSAI3Q7D.js.map → chunk-3QH4NT72.js.map} +0 -0
- /package/dist/{chunk-CTEUPJMV.js.map → chunk-42YP6FMP.js.map} +0 -0
- /package/dist/{chunk-C6HPNPNK.js.map → chunk-53X6CL3A.js.map} +0 -0
- /package/dist/{chunk-U36GYZ2P.js.map → chunk-55NZVUNI.js.map} +0 -0
- /package/dist/{chunk-OEBUAC3O.js.map → chunk-76GQBIVX.js.map} +0 -0
- /package/dist/{chunk-5S7SNRSJ.js.map → chunk-BCQU44UV.js.map} +0 -0
- /package/dist/{chunk-ZQTYWSO3.js.map → chunk-GLJNGCCM.js.map} +0 -0
- /package/dist/{chunk-SMLKHXPF.js.map → chunk-HGG63H3A.js.map} +0 -0
- /package/dist/{chunk-P62DSJUK.js.map → chunk-IAKC3KEJ.js.map} +0 -0
- /package/dist/{chunk-3BTHXB7Z.js.map → chunk-JWG43U5K.js.map} +0 -0
- /package/dist/{chunk-AXGJMDBS.js.map → chunk-KO4X4WRJ.js.map} +0 -0
- /package/dist/{chunk-HJ3CBZIG.js.map → chunk-N4JZ7J2I.js.map} +0 -0
- /package/dist/{chunk-VOZNBQNL.js.map → chunk-NHFWYPXN.js.map} +0 -0
- /package/dist/{chunk-GXETU7AE.js.map → chunk-NI5MWLM3.js.map} +0 -0
- /package/dist/{chunk-MZAH3CBT.js.map → chunk-NWBYJ4VH.js.map} +0 -0
- /package/dist/{chunk-6BG364RN.js.map → chunk-ORVH5S7R.js.map} +0 -0
- /package/dist/{chunk-ANGZWYL2.js.map → chunk-PGJYLYPK.js.map} +0 -0
- /package/dist/{chunk-A3K2KKSG.js.map → chunk-PYYQCHJC.js.map} +0 -0
- /package/dist/{chunk-WHIGRW7T.js.map → chunk-RA3OID2N.js.map} +0 -0
- /package/dist/{chunk-RMV2JYOT.js.map → chunk-SZIJJGA2.js.map} +0 -0
- /package/dist/{chunk-PH2V7EXZ.js.map → chunk-TK7G5ABN.js.map} +0 -0
- /package/dist/{chunk-KYOZ5E6V.js.map → chunk-UEU5IURP.js.map} +0 -0
- /package/dist/{chunk-NSKXRTVZ.js.map → chunk-UGEFYQWU.js.map} +0 -0
- /package/dist/{chunk-AF5PW35O.js.map → chunk-XK6GP7QO.js.map} +0 -0
- /package/dist/{chunk-YHZWVDP7.js.map → chunk-YB5GVH36.js.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
SUPPORT_PASSPORT_MODEL_RESULT_PATH,
|
|
5
5
|
SupportPassportModelBridge,
|
|
6
6
|
parseSupportPassportModelJob
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-UEU5IURP.js";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_SUPPORT_PASSPORT_CONFIG,
|
|
10
10
|
parseSupportPassportConfig
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
buildSupportPassportPublicRequestHandler,
|
|
15
15
|
composeSupportPassportExternalRequestHandlers,
|
|
16
16
|
normalizeSupportPassportNetworkAddress
|
|
17
|
-
} from "../chunk-
|
|
18
|
-
import "../chunk-
|
|
17
|
+
} from "../chunk-NHFWYPXN.js";
|
|
18
|
+
import "../chunk-HGG63H3A.js";
|
|
19
19
|
import {
|
|
20
20
|
SUPPORT_PASSPORT_DRAFT_MAX_CONTENT_CHARACTERS,
|
|
21
21
|
SUPPORT_PASSPORT_DRAFT_MAX_MEMORIES,
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
notifySupportPassportCommitted,
|
|
57
57
|
resolveSupportPassportModelRoutePlan,
|
|
58
58
|
syncDirectoryForDurability
|
|
59
|
-
} from "../chunk-
|
|
59
|
+
} from "../chunk-NI5MWLM3.js";
|
|
60
60
|
import "../chunk-W4F3C7N7.js";
|
|
61
61
|
import "../chunk-MPC7TYTZ.js";
|
|
62
62
|
import "../chunk-ZHXSOMYH.js";
|
|
@@ -88,7 +88,7 @@ import "../chunk-32HO62I4.js";
|
|
|
88
88
|
import "../chunk-SOBJ6NEY.js";
|
|
89
89
|
import "../chunk-QPWUG2S3.js";
|
|
90
90
|
import "../chunk-R36IBI2M.js";
|
|
91
|
-
import "../chunk-
|
|
91
|
+
import "../chunk-53X6CL3A.js";
|
|
92
92
|
import "../chunk-E6UNM67F.js";
|
|
93
93
|
import "../chunk-6WHJ2EAB.js";
|
|
94
94
|
import "../chunk-AQEJKOKI.js";
|
|
@@ -227,13 +227,13 @@ import "../chunk-Z7L72ZHL.js";
|
|
|
227
227
|
import "../chunk-4O4TKIXQ.js";
|
|
228
228
|
import "../chunk-RMMA44HE.js";
|
|
229
229
|
import "../chunk-SYOIVK7K.js";
|
|
230
|
-
import "../chunk-
|
|
230
|
+
import "../chunk-TK7G5ABN.js";
|
|
231
231
|
import "../chunk-MY7B3HYC.js";
|
|
232
232
|
import "../chunk-353C5UNV.js";
|
|
233
233
|
import "../chunk-5AYUHSAU.js";
|
|
234
234
|
import "../chunk-BT7NVCML.js";
|
|
235
235
|
import "../chunk-PO7NXNGC.js";
|
|
236
|
-
import "../chunk-
|
|
236
|
+
import "../chunk-JWG43U5K.js";
|
|
237
237
|
import "../chunk-KQAFEZQX.js";
|
|
238
238
|
import "../chunk-WEHSQBFR.js";
|
|
239
239
|
import "../chunk-X7Y7WX73.js";
|
|
@@ -244,7 +244,7 @@ import "../chunk-SOAU2OE2.js";
|
|
|
244
244
|
import "../chunk-BJMBJZ2Y.js";
|
|
245
245
|
import "../chunk-UKJAGEXH.js";
|
|
246
246
|
import "../chunk-FP2373TW.js";
|
|
247
|
-
import "../chunk-
|
|
247
|
+
import "../chunk-XK6GP7QO.js";
|
|
248
248
|
import "../chunk-N7QV3CMX.js";
|
|
249
249
|
import "../chunk-PAAJWQH5.js";
|
|
250
250
|
import "../chunk-2JRB3Z5N.js";
|
|
@@ -262,13 +262,13 @@ import "../chunk-C7HJ3NEI.js";
|
|
|
262
262
|
import "../chunk-N4PYB235.js";
|
|
263
263
|
import "../chunk-AW4QUFQF.js";
|
|
264
264
|
import "../chunk-5FVVY2EB.js";
|
|
265
|
-
import "../chunk-
|
|
265
|
+
import "../chunk-KJKTDMAD.js";
|
|
266
266
|
import "../chunk-UI3NYK34.js";
|
|
267
|
-
import "../chunk-
|
|
268
|
-
import "../chunk-
|
|
269
|
-
import "../chunk-
|
|
267
|
+
import "../chunk-3E6DGSGZ.js";
|
|
268
|
+
import "../chunk-3QH4NT72.js";
|
|
269
|
+
import "../chunk-KO4X4WRJ.js";
|
|
270
270
|
import "../chunk-TYT4TEQV.js";
|
|
271
|
-
import "../chunk-
|
|
271
|
+
import "../chunk-ANPRNTPT.js";
|
|
272
272
|
import "../chunk-EFHNFTWB.js";
|
|
273
273
|
import "../chunk-GCGJW34D.js";
|
|
274
274
|
import "../chunk-75PT4N7Q.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.69.
|
|
3
|
+
"version": "9.69.99",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3327,7 +3327,7 @@
|
|
|
3327
3327
|
"core"
|
|
3328
3328
|
],
|
|
3329
3329
|
"peerDependencies": {
|
|
3330
|
-
"@remnic/coding-graph": "^9.69.
|
|
3330
|
+
"@remnic/coding-graph": "^9.69.99"
|
|
3331
3331
|
},
|
|
3332
3332
|
"peerDependenciesMeta": {
|
|
3333
3333
|
"@remnic/coding-graph": {
|
|
@@ -104,11 +104,11 @@ export function globToRegExp(glob: string): RegExp {
|
|
|
104
104
|
// (Cursor review on PR #1793: trailing `scratch/**` must match
|
|
105
105
|
// nested `scratch/a/b.md`, matching the offline-mode guide.)
|
|
106
106
|
if (glob[i + 2] === "/") {
|
|
107
|
-
source += "(
|
|
107
|
+
source += "(?:[\\s\\S]*/)?";
|
|
108
108
|
i += 2;
|
|
109
109
|
continue;
|
|
110
110
|
}
|
|
111
|
-
source += "
|
|
111
|
+
source += "[\\s\\S]*";
|
|
112
112
|
i += 1;
|
|
113
113
|
continue;
|
|
114
114
|
}
|
|
@@ -128,6 +128,7 @@ export function globToRegExp(glob: string): RegExp {
|
|
|
128
128
|
return new RegExp(`^${source}$`);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** Match relative POSIX paths: `*` stays within a segment; `**` crosses separators and matches every path alone. */
|
|
131
132
|
export function compileOfflineSyncExcludeGlobs(
|
|
132
133
|
globs: readonly unknown[],
|
|
133
134
|
): RegExp[] {
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit coverage for offline-sync-upload-staging.ts (issue #3164): the real
|
|
3
|
+
* on-disk staging layout under a temp archive root — content-keyed spools,
|
|
4
|
+
* chunk offset contiguity, crash debris reclamation, symlink defenses, and
|
|
5
|
+
* the mid-flight reset a second writer performs at offset 0. These functions
|
|
6
|
+
* are also the write path for remote-authoritative runtime state (#3165).
|
|
7
|
+
*/
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
lstat,
|
|
14
|
+
mkdir,
|
|
15
|
+
mkdtemp,
|
|
16
|
+
readdir,
|
|
17
|
+
readFile,
|
|
18
|
+
rm,
|
|
19
|
+
symlink,
|
|
20
|
+
utimes,
|
|
21
|
+
writeFile,
|
|
22
|
+
} from "node:fs/promises";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
|
|
25
|
+
import { prepareSafeArchiveRoot, sha256Bytes, type SafeArchiveRoot } from "./transfer/fs-utils.js";
|
|
26
|
+
import { SYNC_INTERNAL_DIR } from "./offline-sync-file-io.js";
|
|
27
|
+
import { applyOfflineSyncFileContentChunk } from "./offline-sync.js";
|
|
28
|
+
import {
|
|
29
|
+
cleanupOfflineUpload,
|
|
30
|
+
digestOfflineUploadStagingContent,
|
|
31
|
+
hashText,
|
|
32
|
+
pruneOfflineUploadStaging,
|
|
33
|
+
writeOfflineUploadChunk,
|
|
34
|
+
writeSafeFileFromUpload,
|
|
35
|
+
type OfflineUploadStaging,
|
|
36
|
+
} from "./offline-sync-upload-staging.js";
|
|
37
|
+
|
|
38
|
+
const SHA_A = "a".repeat(64);
|
|
39
|
+
|
|
40
|
+
let rootDirCounter = 0;
|
|
41
|
+
|
|
42
|
+
async function newRoot(): Promise<{ root: SafeArchiveRoot; abs: string }> {
|
|
43
|
+
const abs = await mkdtemp(path.join(tmpdir(), `upload-staging-${process.pid}-${rootDirCounter++}-`));
|
|
44
|
+
const root = await prepareSafeArchiveRoot(abs, "test-root", "root");
|
|
45
|
+
return { root, abs };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function uploadsDir(abs: string): string {
|
|
49
|
+
return path.join(abs, SYNC_INTERNAL_DIR, "uploads");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Drive an upload the way applyOfflineSyncFileContentChunk does, with a fixed chunk size. */
|
|
53
|
+
async function stagedUpload(options: {
|
|
54
|
+
root: SafeArchiveRoot;
|
|
55
|
+
abs: string;
|
|
56
|
+
relPath?: string;
|
|
57
|
+
content: Buffer;
|
|
58
|
+
chunkSize: number;
|
|
59
|
+
sourceId?: string;
|
|
60
|
+
}): Promise<string> {
|
|
61
|
+
const relPath = options.relPath ?? "namespaces/alpha/facts/fact-1.md";
|
|
62
|
+
const sourceId = options.sourceId ?? "source-1";
|
|
63
|
+
const sha256 = sha256Bytes(options.content).sha256;
|
|
64
|
+
let upload: OfflineUploadStaging | null = null;
|
|
65
|
+
try {
|
|
66
|
+
if (options.content.length === 0) {
|
|
67
|
+
upload = await writeOfflineUploadChunk({
|
|
68
|
+
root: options.root,
|
|
69
|
+
sourceId,
|
|
70
|
+
relPath,
|
|
71
|
+
sha256,
|
|
72
|
+
bytes: 0,
|
|
73
|
+
offset: 0,
|
|
74
|
+
content: Buffer.alloc(0),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (let offset = 0; offset < options.content.length; offset += options.chunkSize) {
|
|
78
|
+
const end = Math.min(offset + options.chunkSize, options.content.length);
|
|
79
|
+
upload = await writeOfflineUploadChunk({
|
|
80
|
+
root: options.root,
|
|
81
|
+
sourceId,
|
|
82
|
+
relPath,
|
|
83
|
+
sha256,
|
|
84
|
+
bytes: options.content.length,
|
|
85
|
+
offset,
|
|
86
|
+
content: options.content.subarray(offset, end),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (upload === null) throw new Error("unreachable: the empty-content path still writes chunk 0");
|
|
90
|
+
const digest = await digestOfflineUploadStagingContent({ root: options.root, upload });
|
|
91
|
+
assert.equal(digest.sha256, sha256);
|
|
92
|
+
assert.equal(digest.bytes, options.content.length);
|
|
93
|
+
await writeSafeFileFromUpload(options.root, relPath, upload, undefined, undefined, undefined);
|
|
94
|
+
return path.join(options.abs, relPath);
|
|
95
|
+
} finally {
|
|
96
|
+
if (upload) await cleanupOfflineUpload(upload);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test("multi-chunk upload writes exact bytes and reclaims its staging", async (t) => {
|
|
101
|
+
const { root, abs } = await newRoot();
|
|
102
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
103
|
+
const content = Buffer.from("abcdefghij", "utf8");
|
|
104
|
+
const target = await stagedUpload({ root, abs, content, chunkSize: 4 });
|
|
105
|
+
assert.equal(await readFile(target, "utf8"), "abcdefghij");
|
|
106
|
+
assert.deepEqual(await readdir(uploadsDir(abs)), [], "staging reclaimed after finalize");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("empty upload (bytes 0) still creates an empty target file", async (t) => {
|
|
110
|
+
const { root, abs } = await newRoot();
|
|
111
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
112
|
+
const target = await stagedUpload({ root, abs, content: Buffer.alloc(0), chunkSize: 4 });
|
|
113
|
+
const stat = await lstat(target);
|
|
114
|
+
assert.equal(stat.size, 0, "empty upload creates a zero-byte target");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("chunk above offset 0 without the initial chunk fails loudly", async (t) => {
|
|
118
|
+
const { root, abs } = await newRoot();
|
|
119
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
120
|
+
await assert.rejects(
|
|
121
|
+
writeOfflineUploadChunk({
|
|
122
|
+
root,
|
|
123
|
+
sourceId: "source-1",
|
|
124
|
+
relPath: "namespaces/alpha/facts/fact-1.md",
|
|
125
|
+
sha256: SHA_A,
|
|
126
|
+
bytes: 100,
|
|
127
|
+
offset: 4,
|
|
128
|
+
content: Buffer.from("abcd"),
|
|
129
|
+
}),
|
|
130
|
+
/missing initial chunk/,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("digest refuses a gapped chunk sequence instead of skipping bytes", async (t) => {
|
|
135
|
+
const { root, abs } = await newRoot();
|
|
136
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
137
|
+
const relPath = "namespaces/alpha/facts/fact-1.md";
|
|
138
|
+
const upload = await writeOfflineUploadChunk({
|
|
139
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 100, offset: 0,
|
|
140
|
+
content: Buffer.from("aaaa"),
|
|
141
|
+
});
|
|
142
|
+
await writeOfflineUploadChunk({
|
|
143
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 100, offset: 9,
|
|
144
|
+
content: Buffer.from("bbbb"),
|
|
145
|
+
});
|
|
146
|
+
await assert.rejects(
|
|
147
|
+
digestOfflineUploadStagingContent({ root, upload }),
|
|
148
|
+
/offset mismatch .* expected 4, got 9/,
|
|
149
|
+
);
|
|
150
|
+
await cleanupOfflineUpload(upload);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a second writer's offset-0 reset converges identical content and corrupts nothing", async (t) => {
|
|
154
|
+
const { root, abs } = await newRoot();
|
|
155
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
156
|
+
const relPath = "namespaces/alpha/facts/fact-1.md";
|
|
157
|
+
const declared = sha256Bytes(Buffer.from("12345678")).sha256;
|
|
158
|
+
const shared = { root, sourceId: "source-1", relPath, sha256: declared, bytes: 8 };
|
|
159
|
+
const first = await writeOfflineUploadChunk({ ...shared, offset: 0, content: Buffer.from("1234") });
|
|
160
|
+
await writeOfflineUploadChunk({ ...shared, offset: 0, content: Buffer.from("1234") });
|
|
161
|
+
await writeOfflineUploadChunk({ ...shared, offset: 4, content: Buffer.from("5678") });
|
|
162
|
+
const digest = await digestOfflineUploadStagingContent({ root, upload: first });
|
|
163
|
+
assert.deepEqual(digest, { sha256: declared, bytes: 8 });
|
|
164
|
+
await writeSafeFileFromUpload(root, relPath, first, undefined, undefined, undefined);
|
|
165
|
+
assert.equal(await readFile(path.join(abs, relPath), "utf8"), "12345678");
|
|
166
|
+
await cleanupOfflineUpload(first);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a reset carrying different bytes fails the real consumer's checksum gate", async (t) => {
|
|
170
|
+
const { abs } = await newRoot();
|
|
171
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
172
|
+
const relPath = "namespaces/alpha/facts/fact-1.md";
|
|
173
|
+
const declared = sha256Bytes(Buffer.from("12345678")).sha256;
|
|
174
|
+
const shared = {
|
|
175
|
+
root: abs,
|
|
176
|
+
sourceId: "source-1",
|
|
177
|
+
path: relPath,
|
|
178
|
+
sha256: declared,
|
|
179
|
+
bytes: 8,
|
|
180
|
+
mtimeMs: 1_700_000_000_000,
|
|
181
|
+
};
|
|
182
|
+
await applyOfflineSyncFileContentChunk({ ...shared, offset: 0, content: Buffer.from("1234") });
|
|
183
|
+
await applyOfflineSyncFileContentChunk({ ...shared, offset: 0, content: Buffer.from("9999") });
|
|
184
|
+
await assert.rejects(
|
|
185
|
+
applyOfflineSyncFileContentChunk({ ...shared, offset: 4, content: Buffer.from("5678") }),
|
|
186
|
+
/checksum mismatch/,
|
|
187
|
+
);
|
|
188
|
+
assert.deepEqual(
|
|
189
|
+
await readdir(uploadsDir(abs)),
|
|
190
|
+
[],
|
|
191
|
+
"the consumer reclaimed the failed spool",
|
|
192
|
+
);
|
|
193
|
+
assert.equal(fs.existsSync(path.join(abs, relPath)), false, "target never written from a bad spool");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("crash debris: abandoned staging is reclaimed only after the 24h age, by pattern", async (t) => {
|
|
197
|
+
const { root, abs } = await newRoot();
|
|
198
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
199
|
+
const relPath = "namespaces/alpha/facts/fact-1.md";
|
|
200
|
+
const stagingKey = hashText(["source-1", relPath, SHA_A, "8"].join("\0"));
|
|
201
|
+
const stage = path.join(uploadsDir(abs), `${stagingKey}.part`);
|
|
202
|
+
await writeOfflineUploadChunk({
|
|
203
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 0, content: Buffer.from("1234"),
|
|
204
|
+
});
|
|
205
|
+
assert.equal((await readdir(uploadsDir(abs))).length, 1);
|
|
206
|
+
|
|
207
|
+
await pruneOfflineUploadStaging(root);
|
|
208
|
+
assert.equal((await readdir(uploadsDir(abs))).length, 1, "fresh abandoned staging is kept");
|
|
209
|
+
|
|
210
|
+
const stale = new Date(Date.now() - 25 * 60 * 60 * 1000);
|
|
211
|
+
await utimes(stage, stale, stale);
|
|
212
|
+
await pruneOfflineUploadStaging(root);
|
|
213
|
+
assert.equal((await readdir(uploadsDir(abs))).length, 0, "aged-out staging is reclaimed");
|
|
214
|
+
|
|
215
|
+
await writeFile(path.join(uploadsDir(abs), "not-a-part"), "keep me");
|
|
216
|
+
await writeFile(path.join(uploadsDir(abs), `${"z".repeat(64)}.part`), "keep me too");
|
|
217
|
+
await utimes(path.join(uploadsDir(abs), "not-a-part"), stale, stale);
|
|
218
|
+
await utimes(path.join(uploadsDir(abs), `${"z".repeat(64)}.part`), stale, stale);
|
|
219
|
+
await pruneOfflineUploadStaging(root);
|
|
220
|
+
assert.deepEqual((await readdir(uploadsDir(abs))).sort(), [
|
|
221
|
+
"not-a-part",
|
|
222
|
+
`${"z".repeat(64)}.part`,
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("symlinked chunk path is rejected before write", async (t) => {
|
|
227
|
+
const { root, abs } = await newRoot();
|
|
228
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
229
|
+
const relPath = "namespaces/alpha/facts/fact-1.md";
|
|
230
|
+
const stagingKey = hashText(["source-1", relPath, SHA_A, "8"].join("\0"));
|
|
231
|
+
const stage = path.join(uploadsDir(abs), `${stagingKey}.part`);
|
|
232
|
+
await writeOfflineUploadChunk({
|
|
233
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 0, content: Buffer.from("1234"),
|
|
234
|
+
});
|
|
235
|
+
const victim = path.join(abs, "victim.txt");
|
|
236
|
+
await writeFile(victim, "do not touch");
|
|
237
|
+
await symlink(victim, path.join(stage, `${String(4).padStart(20, "0")}.part`));
|
|
238
|
+
await assert.rejects(
|
|
239
|
+
writeOfflineUploadChunk({
|
|
240
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 4, content: Buffer.from("5678"),
|
|
241
|
+
}),
|
|
242
|
+
/symlink/,
|
|
243
|
+
);
|
|
244
|
+
assert.equal(await readFile(victim, "utf8"), "do not touch", "symlink target untouched");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("symlinked final target is rejected and no tmp file is left behind", async (t) => {
|
|
248
|
+
const { root, abs } = await newRoot();
|
|
249
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
250
|
+
const relPath = "namespaces/alpha/facts/fact-1.md";
|
|
251
|
+
const upload = await writeOfflineUploadChunk({
|
|
252
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 8, offset: 0, content: Buffer.from("1234"),
|
|
253
|
+
});
|
|
254
|
+
const realTarget = path.join(abs, "elsewhere.md");
|
|
255
|
+
await writeFile(realTarget, "existing file");
|
|
256
|
+
await mkdir(path.dirname(path.join(abs, relPath)), { recursive: true });
|
|
257
|
+
await symlink(realTarget, path.join(abs, relPath));
|
|
258
|
+
await assert.rejects(
|
|
259
|
+
writeSafeFileFromUpload(root, relPath, upload, undefined, undefined, undefined),
|
|
260
|
+
/symlink/,
|
|
261
|
+
);
|
|
262
|
+
assert.equal(await readFile(realTarget, "utf8"), "existing file", "symlink target not overwritten");
|
|
263
|
+
const leftovers = (await readdir(path.dirname(path.join(abs, relPath))))
|
|
264
|
+
.filter((name) => name.startsWith(".remnic-sync."));
|
|
265
|
+
assert.deepEqual(leftovers, [], "atomic-write tmp cleaned up");
|
|
266
|
+
await cleanupOfflineUpload(upload);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("writeFileChunks hook receives every chunk in order without a local tmp", async (t) => {
|
|
270
|
+
const { root, abs } = await newRoot();
|
|
271
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
272
|
+
const relPath = "namespaces/alpha/facts/fact-1.md";
|
|
273
|
+
const upload = await writeOfflineUploadChunk({
|
|
274
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 12, offset: 0, content: Buffer.from("1234"),
|
|
275
|
+
});
|
|
276
|
+
await writeOfflineUploadChunk({
|
|
277
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 12, offset: 4, content: Buffer.from("5678"),
|
|
278
|
+
});
|
|
279
|
+
await writeOfflineUploadChunk({
|
|
280
|
+
root, sourceId: "source-1", relPath, sha256: SHA_A, bytes: 12, offset: 8, content: Buffer.from("90ab"),
|
|
281
|
+
});
|
|
282
|
+
const streamed: Buffer[] = [];
|
|
283
|
+
const mtimeMs = 1_700_000_000_000;
|
|
284
|
+
await writeSafeFileFromUpload(root, relPath, upload, undefined, async (target) => {
|
|
285
|
+
assert.equal(target.path, relPath);
|
|
286
|
+
for await (const chunk of target.chunks) streamed.push(Buffer.from(chunk));
|
|
287
|
+
}, mtimeMs);
|
|
288
|
+
assert.equal(Buffer.concat(streamed).toString("utf8"), "1234567890ab");
|
|
289
|
+
assert.equal(
|
|
290
|
+
fs.existsSync(path.join(abs, relPath)),
|
|
291
|
+
false,
|
|
292
|
+
"hook path does not also write a local file",
|
|
293
|
+
);
|
|
294
|
+
await cleanupOfflineUpload(upload);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("concurrent staging keeps identical content at different paths isolated", async (t) => {
|
|
298
|
+
const { root, abs } = await newRoot();
|
|
299
|
+
t.after(() => rm(abs, { recursive: true, force: true }));
|
|
300
|
+
const content = Buffer.from("12345678");
|
|
301
|
+
const shared = { root, sourceId: "source-1", ...sha256Bytes(content) };
|
|
302
|
+
const firstPath = "namespaces/alpha/facts/one.md";
|
|
303
|
+
const secondPath = "namespaces/alpha/facts/two.md";
|
|
304
|
+
const first = await writeOfflineUploadChunk({
|
|
305
|
+
...shared, relPath: firstPath, offset: 0, content: content.subarray(0, 4),
|
|
306
|
+
});
|
|
307
|
+
const second = await writeOfflineUploadChunk({
|
|
308
|
+
...shared, relPath: secondPath, offset: 0, content: content.subarray(0, 4),
|
|
309
|
+
});
|
|
310
|
+
await writeOfflineUploadChunk({
|
|
311
|
+
...shared, relPath: firstPath, offset: 4, content: content.subarray(4),
|
|
312
|
+
});
|
|
313
|
+
assert.deepEqual(await digestOfflineUploadStagingContent({ root, upload: first }), sha256Bytes(content));
|
|
314
|
+
await writeSafeFileFromUpload(root, firstPath, first);
|
|
315
|
+
await cleanupOfflineUpload(first);
|
|
316
|
+
await writeOfflineUploadChunk({
|
|
317
|
+
...shared, relPath: secondPath, offset: 4, content: content.subarray(4),
|
|
318
|
+
});
|
|
319
|
+
assert.deepEqual(await digestOfflineUploadStagingContent({ root, upload: second }), sha256Bytes(content));
|
|
320
|
+
await writeSafeFileFromUpload(root, secondPath, second);
|
|
321
|
+
await cleanupOfflineUpload(second);
|
|
322
|
+
assert.deepEqual(await readFile(path.join(abs, firstPath)), content);
|
|
323
|
+
assert.deepEqual(await readFile(path.join(abs, secondPath)), content);
|
|
324
|
+
});
|
|
@@ -19,7 +19,8 @@ export function hashText(value: string): string {
|
|
|
19
19
|
return createHash("sha256").update(value).digest("hex");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/** A staged upload spool returned by writeOfflineUploadChunk; consumed by digest/cleanup/finalize. */
|
|
23
|
+
export interface OfflineUploadStaging {
|
|
23
24
|
kind: "single" | "chunks";
|
|
24
25
|
relPath: string;
|
|
25
26
|
filePath: string;
|
package/src/offline-sync.test.ts
CHANGED
|
@@ -3077,6 +3077,18 @@ test("globToRegExp treats ** as cross-segment in trailing and embedded positions
|
|
|
3077
3077
|
assert.equal(embedded.test("state/a/b/other.json"), false);
|
|
3078
3078
|
});
|
|
3079
3079
|
|
|
3080
|
+
test("globToRegExp ** spans line terminators that valid archive paths may contain (#3172)", () => {
|
|
3081
|
+
const trailing = globToRegExp("scratch/**");
|
|
3082
|
+
assert.equal(trailing.test("scratch/a\nb.md"), true);
|
|
3083
|
+
assert.equal(trailing.test("scratch/a\nb/c.md"), true);
|
|
3084
|
+
const embedded = globToRegExp("state/**/runs.json");
|
|
3085
|
+
assert.equal(embedded.test("state/a\nb/runs.json"), true);
|
|
3086
|
+
assert.equal(globToRegExp("**/state/*.sqlite").test("a\nb/state/lcm.sqlite"), true);
|
|
3087
|
+
const single = globToRegExp("state/*.sqlite");
|
|
3088
|
+
assert.equal(single.test("state/a\nb.sqlite"), true);
|
|
3089
|
+
assert.equal(single.test("state/a/b.sqlite"), false);
|
|
3090
|
+
});
|
|
3091
|
+
|
|
3080
3092
|
test("default excludes cover per-namespace state dirs (#1793 review)", () => {
|
|
3081
3093
|
const sqlite = globToRegExp("**/state/*.sqlite");
|
|
3082
3094
|
assert.equal(sqlite.test("state/lcm.sqlite"), true);
|
package/src/offline-sync.ts
CHANGED
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
writeOfflineUploadChunk,
|
|
77
77
|
writeSafeFileFromUpload,
|
|
78
78
|
hashText,
|
|
79
|
+
type OfflineUploadStaging,
|
|
79
80
|
} from "./offline-sync-upload-staging.js";
|
|
80
81
|
export type { OfflineSyncExcludeFile, OfflineSyncFileTarget } from "./offline-sync-file-io.js";
|
|
81
82
|
|
|
@@ -265,12 +266,6 @@ export interface OfflineSyncApplyFileContentChunkResult {
|
|
|
265
266
|
currentFile?: OfflineSyncFileState;
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
interface OfflineUploadStaging {
|
|
269
|
-
kind: "single" | "chunks";
|
|
270
|
-
relPath: string;
|
|
271
|
-
filePath: string;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
269
|
interface OfflineSyncFileRecordOptions {
|
|
275
270
|
root: SafeArchiveRoot;
|
|
276
271
|
relPath: string;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/offline-sync-upload-staging.ts"],"sourcesContent":["// Offline-sync upload staging: chunked upload spool under\n// `.offline-sync/uploads/` (issue #2033). Extracted from offline-sync.ts so\n// the god-file line-count ratchet does not grow; behavior is unchanged.\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { lstat, mkdir, open, readFile, readdir, rename, rm, stat, unlink, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { SYNC_INTERNAL_DIR, setSafeFileMtime } from \"./offline-sync-file-io.js\";\nimport type { OfflineSyncFileTarget } from \"./offline-sync-file-io.js\";\nimport type {\n OfflineSyncFileStagingWriteTarget,\n OfflineSyncFileWriteChunksTarget,\n OfflineSyncFileWriteTarget,\n} from \"./offline-sync.js\";\nimport { type SafeArchiveRoot, resolveSafeArchiveTarget } from \"./transfer/fs-utils.js\";\n\nconst OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1000;\n\nexport function hashText(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\ninterface OfflineUploadStaging {\n kind: \"single\" | \"chunks\";\n relPath: string;\n filePath: string;\n}\n\nfunction offlineUploadRelPath(options: {\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n}): string {\n const key = hashText([options.sourceId, options.relPath, options.sha256, String(options.bytes)].join(\"\\0\"));\n return `${SYNC_INTERNAL_DIR}/uploads/${key}.part`;\n}\n\nasync function offlineUploadPath(\n root: SafeArchiveRoot,\n options: {\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n }\n): Promise<OfflineUploadStaging> {\n const relPath = offlineUploadRelPath(options);\n return {\n kind: \"single\",\n relPath,\n filePath: await resolveSafeArchiveTarget(root, relPath),\n };\n}\n\nasync function offlineUploadChunkPath(\n root: SafeArchiveRoot,\n options: {\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n offset: number;\n }\n): Promise<OfflineUploadStaging> {\n const uploadRelPath = offlineUploadRelPath(options);\n const relPath = `${uploadRelPath}/${String(options.offset).padStart(20, \"0\")}.part`;\n return {\n kind: \"chunks\",\n relPath,\n filePath: await resolveSafeArchiveTarget(root, relPath),\n };\n}\n\nexport async function writeOfflineUploadChunk(options: {\n root: SafeArchiveRoot;\n sourceId: string;\n relPath: string;\n sha256: string;\n bytes: number;\n offset: number;\n content: Buffer;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n writeFile?: (target: OfflineSyncFileWriteTarget) => Promise<void>;\n writeStagingFile?: (target: OfflineSyncFileStagingWriteTarget) => Promise<void>;\n}): Promise<OfflineUploadStaging> {\n if ((options.writeFile || options.writeStagingFile) && !options.readFile) {\n throw new Error(\"offline sync upload chunk storage hooks require readFile\");\n }\n const uploadRoot = {\n ...(await offlineUploadPath(options.root, options)),\n kind: \"chunks\" as const,\n };\n if (options.offset === 0) {\n await rm(uploadRoot.filePath, { recursive: true, force: true }).catch(() => {});\n } else {\n const existing = await stat(uploadRoot.filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (!existing || !existing.isDirectory()) {\n throw new Error(`offline sync upload is missing initial chunk for ${options.relPath}`);\n }\n }\n const chunk = await offlineUploadChunkPath(options.root, { ...options, offset: options.offset });\n\n const writeStagingFile = options.writeStagingFile ?? options.writeFile;\n if (writeStagingFile) {\n // Storage-backed services provide these hooks so secure-store deployments\n // keep staged partial uploads encrypted at rest without mutating indexes.\n await writeOfflineUploadContent({\n root: options.root,\n relPath: chunk.relPath,\n filePath: chunk.filePath,\n content: options.content,\n writeFile: writeStagingFile,\n });\n return uploadRoot;\n }\n\n await mkdir(path.dirname(chunk.filePath), { recursive: true });\n const existingChunk = await lstat(chunk.filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (existingChunk?.isSymbolicLink()) {\n throw new Error(`offline sync upload chunk is a symlink: ${chunk.relPath}`);\n }\n await writeFile(chunk.filePath, options.content, { mode: 0o600 });\n return uploadRoot;\n}\n\nexport async function pruneOfflineUploadStaging(root: SafeArchiveRoot): Promise<void> {\n const uploadsRelPath = `${SYNC_INTERNAL_DIR}/uploads`;\n const uploadsPath = await resolveSafeArchiveTarget(root, uploadsRelPath);\n const entries = await readdir(uploadsPath, { withFileTypes: true }).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return [];\n throw error;\n });\n const now = Date.now();\n await Promise.all(\n entries.map(async (entry) => {\n if (!/^[a-f0-9]{64}\\.part$/i.test(entry.name)) return;\n const relPath = `${uploadsRelPath}/${entry.name}`;\n const filePath = await resolveSafeArchiveTarget(root, relPath);\n const info = await lstat(filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (!info) return;\n if (now - info.mtimeMs <= OFFLINE_SYNC_UPLOAD_STAGING_MAX_AGE_MS) return;\n await rm(filePath, { recursive: true, force: true });\n })\n );\n}\n\nasync function* readOfflineUploadStagingChunks(options: {\n root: SafeArchiveRoot;\n upload: OfflineUploadStaging;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n}): AsyncGenerator<Buffer> {\n if (options.upload.kind === \"single\") {\n yield await readOfflineUploadContent({\n root: options.root,\n relPath: options.upload.relPath,\n filePath: options.upload.filePath,\n readFile: options.readFile,\n });\n return;\n }\n\n const entries = await readdir(options.upload.filePath);\n const chunkNames = entries.filter((entry) => /^\\d{20}\\.part$/.test(entry)).sort();\n if (chunkNames.length === 0) {\n throw new Error(`offline sync upload is missing chunks for ${options.upload.relPath}`);\n }\n let expectedOffset = 0;\n for (const chunkName of chunkNames) {\n const offset = Number(chunkName.slice(0, 20));\n if (!Number.isSafeInteger(offset) || offset !== expectedOffset) {\n throw new Error(\n `offline sync upload offset mismatch for ${options.upload.relPath}: expected ${expectedOffset}, got ${offset}`\n );\n }\n const relPath = `${options.upload.relPath}/${chunkName}`;\n const filePath = await resolveSafeArchiveTarget(options.root, relPath);\n const content = await readOfflineUploadContent({\n root: options.root,\n relPath,\n filePath,\n readFile: options.readFile,\n });\n expectedOffset += content.length;\n yield content;\n }\n}\n\nexport async function digestOfflineUploadStagingContent(options: {\n root: SafeArchiveRoot;\n upload: OfflineUploadStaging;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n}): Promise<{ sha256: string; bytes: number }> {\n const hash = createHash(\"sha256\");\n let bytes = 0;\n for await (const chunk of readOfflineUploadStagingChunks(options)) {\n hash.update(chunk);\n bytes += chunk.length;\n }\n return { sha256: hash.digest(\"hex\"), bytes };\n}\n\nexport async function writeSafeFileFromUpload(\n root: SafeArchiveRoot,\n relPath: string,\n upload: OfflineUploadStaging,\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>,\n writeFileChunks?: (target: OfflineSyncFileWriteChunksTarget) => Promise<void>,\n mtimeMs?: number\n): Promise<void> {\n const target = await resolveSafeArchiveTarget(root, relPath);\n const chunks = readOfflineUploadStagingChunks({ root, upload, readFile });\n if (writeFileChunks) {\n await writeFileChunks({ root: root.abs, path: relPath, filePath: target, chunks });\n await setSafeFileMtime(root, relPath, mtimeMs);\n return;\n }\n\n await mkdir(path.dirname(target), { recursive: true });\n const tmp = path.join(path.dirname(target), `.remnic-sync.${process.pid}.${randomUUID()}.tmp`);\n const handle = await open(tmp, \"w\", 0o600);\n try {\n for await (const chunk of chunks) {\n if (chunk.length > 0) await handle.write(chunk);\n }\n await handle.close();\n const targetStat = await lstat(target).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n });\n if (targetStat?.isSymbolicLink()) {\n throw new Error(`offline sync target is a symlink: ${relPath}`);\n }\n await rename(tmp, target);\n await setSafeFileMtime(root, relPath, mtimeMs);\n } catch (error) {\n await handle.close().catch(() => {});\n await unlink(tmp).catch(() => {});\n throw error;\n }\n}\n\nexport async function cleanupOfflineUpload(upload: OfflineUploadStaging): Promise<void> {\n if (upload.kind === \"chunks\") {\n await rm(upload.filePath, { recursive: true, force: true });\n return;\n }\n await unlink(upload.filePath).catch((error: unknown) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return;\n throw error;\n });\n}\n\nasync function readOfflineUploadContent(options: {\n root: SafeArchiveRoot;\n relPath: string;\n filePath: string;\n readFile?: (target: OfflineSyncFileTarget) => Promise<Buffer>;\n}): Promise<Buffer> {\n if (options.readFile) {\n return options.readFile({\n root: options.root.abs,\n path: options.relPath,\n filePath: options.filePath,\n });\n }\n return readFile(options.filePath);\n}\n\nasync function writeOfflineUploadContent(options: {\n root: SafeArchiveRoot;\n relPath: string;\n filePath: string;\n content: Buffer;\n writeFile: (target: OfflineSyncFileWriteTarget) => Promise<void>;\n}): Promise<void> {\n await options.writeFile({\n root: options.root.abs,\n path: options.relPath,\n filePath: options.filePath,\n content: options.content,\n });\n}\n"],"mappings":"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","names":["readFile"]}
|