@indigoai-us/hq-cloud 6.15.21 → 6.15.22
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/share.d.ts +36 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +139 -20
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +220 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +6 -0
- package/dist/cli/sync.js.map +1 -1
- package/dist/journal.d.ts +1 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +4 -1
- package/dist/journal.js.map +1 -1
- package/dist/types.d.ts +17 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/share.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Broadcasts local file(s) to the company's S3 vault bucket.
|
|
5
5
|
* Refuses to overwrite a newer remote version without prompting.
|
|
6
6
|
*/
|
|
7
|
-
import type { EntityContext, VaultServiceConfig } from "../types.js";
|
|
7
|
+
import type { EntityContext, VaultServiceConfig, JournalEntry } from "../types.js";
|
|
8
8
|
import type { UploadAuthor } from "../s3.js";
|
|
9
9
|
import type { ConflictStrategy } from "./conflict.js";
|
|
10
10
|
import type { SyncProgressEvent } from "./sync.js";
|
|
@@ -163,6 +163,7 @@ export declare const _testing: {
|
|
|
163
163
|
resolveNamedPath: typeof resolveNamedPath;
|
|
164
164
|
isWithinLexicalOrReal: typeof isWithinLexicalOrReal;
|
|
165
165
|
defaultConsoleLogger: typeof defaultConsoleLogger;
|
|
166
|
+
statMatchesJournal: typeof statMatchesJournal;
|
|
166
167
|
};
|
|
167
168
|
/**
|
|
168
169
|
* Stage-1 classification for a single local file in a push run. Pre-HEAD —
|
|
@@ -181,6 +182,7 @@ type PushPlanItem = {
|
|
|
181
182
|
relativePath: string;
|
|
182
183
|
localHash: string;
|
|
183
184
|
size: number;
|
|
185
|
+
statAtHash?: StatSnapshot;
|
|
184
186
|
} | {
|
|
185
187
|
action: "upload";
|
|
186
188
|
kind: "symlink";
|
|
@@ -199,9 +201,42 @@ type PushPlanItem = {
|
|
|
199
201
|
relativePath: string;
|
|
200
202
|
restamp?: {
|
|
201
203
|
mtimeMs: number;
|
|
204
|
+
ctimeMs: number;
|
|
202
205
|
size: number;
|
|
203
206
|
};
|
|
204
207
|
};
|
|
208
|
+
/**
|
|
209
|
+
* Pure Stage-1 pass for push: walk the candidate file list, hash each one,
|
|
210
|
+
* apply the size-limit and skip-unchanged gates, and return a classified
|
|
211
|
+
* plan plus aggregate counts. No S3 calls, no journal writes, no event
|
|
212
|
+
* emission.
|
|
213
|
+
*
|
|
214
|
+
* The conflict count is intentionally absent from the returned `PushPlan` —
|
|
215
|
+
* detecting a push conflict requires a remote HEAD that we defer to Stage 2.
|
|
216
|
+
* Consumers that want a conflict count get it from the `complete` event.
|
|
217
|
+
*/
|
|
218
|
+
/** The three stat fields the skip gate compares. */
|
|
219
|
+
interface StatSnapshot {
|
|
220
|
+
size: number;
|
|
221
|
+
mtimeMs: number;
|
|
222
|
+
ctimeMs: number;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* True when `absolutePath` provably has not been written since its journal
|
|
226
|
+
* entry was stamped, judged from stat alone.
|
|
227
|
+
*
|
|
228
|
+
* Fails closed. Anything unusual about the entry — no stat recorded, a
|
|
229
|
+
* tombstone, a pending local delete, a pull-kept divergence, a symlink record,
|
|
230
|
+
* or a local object that is no longer a plain file — returns false and sends
|
|
231
|
+
* the caller down the hashing path, which is the pre-fast-path behaviour.
|
|
232
|
+
*
|
|
233
|
+
* The three fields are compared exactly, on purpose. `mtimeMs` from `lstat`
|
|
234
|
+
* carries sub-millisecond precision that `utimes` cannot reproduce, so a
|
|
235
|
+
* restored timestamp differs from the original anyway — but that is an
|
|
236
|
+
* accident of precision, not a guarantee, and nothing here leans on it.
|
|
237
|
+
* `ctimeMs` is the field doing the real work.
|
|
238
|
+
*/
|
|
239
|
+
declare function statMatchesJournal(absolutePath: string, entry: JournalEntry | undefined): boolean;
|
|
205
240
|
export interface ShareOptions {
|
|
206
241
|
/** Path(s) to share (files or directories) */
|
|
207
242
|
paths: string[];
|
package/dist/cli/share.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../../src/cli/share.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAe,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../../src/cli/share.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/share.js
CHANGED
|
@@ -236,17 +236,109 @@ export const _testing = {
|
|
|
236
236
|
resolveNamedPath,
|
|
237
237
|
isWithinLexicalOrReal,
|
|
238
238
|
defaultConsoleLogger,
|
|
239
|
+
statMatchesJournal,
|
|
239
240
|
};
|
|
241
|
+
/** Snapshot of `absolutePath`, or null when it is missing or not a plain file. */
|
|
242
|
+
function statSnapshot(absolutePath) {
|
|
243
|
+
try {
|
|
244
|
+
const lstat = fs.lstatSync(absolutePath);
|
|
245
|
+
if (!lstat.isFile())
|
|
246
|
+
return null;
|
|
247
|
+
return { size: lstat.size, mtimeMs: lstat.mtimeMs, ctimeMs: lstat.ctimeMs };
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function sameStatSnapshot(a, b) {
|
|
254
|
+
return (a !== null &&
|
|
255
|
+
b !== null &&
|
|
256
|
+
a.size === b.size &&
|
|
257
|
+
a.mtimeMs === b.mtimeMs &&
|
|
258
|
+
a.ctimeMs === b.ctimeMs);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The `ctimeMs` it is safe to stamp alongside a hash, or undefined.
|
|
262
|
+
*
|
|
263
|
+
* `hashedStat` is the stat the hash was computed against. If the file has moved
|
|
264
|
+
* since, the hash in hand describes bytes that are no longer on disk, and
|
|
265
|
+
* pairing it with the current timestamps would let every later pass skip the
|
|
266
|
+
* new body on stat alone. Returning undefined leaves the entry without a ctime,
|
|
267
|
+
* which the skip gate refuses — so the next pass hashes it.
|
|
268
|
+
*/
|
|
269
|
+
function ctimeToStamp(hashedStat, lstat) {
|
|
270
|
+
if (!hashedStat)
|
|
271
|
+
return undefined;
|
|
272
|
+
const current = {
|
|
273
|
+
size: lstat.size,
|
|
274
|
+
mtimeMs: lstat.mtimeMs,
|
|
275
|
+
ctimeMs: lstat.ctimeMs,
|
|
276
|
+
};
|
|
277
|
+
return sameStatSnapshot(hashedStat, current) ? lstat.ctimeMs : undefined;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* True when a timestamp pair is too coarse to be evidence of anything.
|
|
281
|
+
*
|
|
282
|
+
* FAT/exFAT, some FUSE mounts and cached network filesystems round timestamps
|
|
283
|
+
* to whole seconds (FAT's mtime granularity is two), and may synthesize ctime
|
|
284
|
+
* from mtime. On such a volume a same-size rewrite inside one tick leaves both
|
|
285
|
+
* fields equal to the journal's, and the skip gate would strand the edit
|
|
286
|
+
* forever. Every filesystem this gate can safely run on — APFS, ext4, NTFS —
|
|
287
|
+
* records sub-second precision, so a pair sitting exactly on the second is
|
|
288
|
+
* treated as untrustworthy and sent back to hashing.
|
|
289
|
+
*
|
|
290
|
+
* The cost of the false positive is one hash: on a real filesystem a file whose
|
|
291
|
+
* mtime AND ctime both land exactly on a second boundary is rare, and it is
|
|
292
|
+
* simply hashed as it was before the fast path existed.
|
|
293
|
+
*/
|
|
294
|
+
function timestampsTooCoarseToTrust(mtimeMs, ctimeMs) {
|
|
295
|
+
return mtimeMs % 1000 === 0 && ctimeMs % 1000 === 0;
|
|
296
|
+
}
|
|
240
297
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
298
|
+
* True when `absolutePath` provably has not been written since its journal
|
|
299
|
+
* entry was stamped, judged from stat alone.
|
|
300
|
+
*
|
|
301
|
+
* Fails closed. Anything unusual about the entry — no stat recorded, a
|
|
302
|
+
* tombstone, a pending local delete, a pull-kept divergence, a symlink record,
|
|
303
|
+
* or a local object that is no longer a plain file — returns false and sends
|
|
304
|
+
* the caller down the hashing path, which is the pre-fast-path behaviour.
|
|
245
305
|
*
|
|
246
|
-
* The
|
|
247
|
-
*
|
|
248
|
-
*
|
|
306
|
+
* The three fields are compared exactly, on purpose. `mtimeMs` from `lstat`
|
|
307
|
+
* carries sub-millisecond precision that `utimes` cannot reproduce, so a
|
|
308
|
+
* restored timestamp differs from the original anyway — but that is an
|
|
309
|
+
* accident of precision, not a guarantee, and nothing here leans on it.
|
|
310
|
+
* `ctimeMs` is the field doing the real work.
|
|
249
311
|
*/
|
|
312
|
+
function statMatchesJournal(absolutePath, entry) {
|
|
313
|
+
if (!entry)
|
|
314
|
+
return false;
|
|
315
|
+
if (!entry.hash)
|
|
316
|
+
return false;
|
|
317
|
+
if (entry.kind === "symlink")
|
|
318
|
+
return false;
|
|
319
|
+
if (entry.localDiverges)
|
|
320
|
+
return false;
|
|
321
|
+
if (entry.removedAt !== undefined)
|
|
322
|
+
return false;
|
|
323
|
+
if (entry.localDeleteIntent !== undefined)
|
|
324
|
+
return false;
|
|
325
|
+
if (entry.mtimeMs === undefined || entry.ctimeMs === undefined)
|
|
326
|
+
return false;
|
|
327
|
+
if (timestampsTooCoarseToTrust(entry.mtimeMs, entry.ctimeMs))
|
|
328
|
+
return false;
|
|
329
|
+
let lstat;
|
|
330
|
+
try {
|
|
331
|
+
lstat = fs.lstatSync(absolutePath);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
if (!lstat.isFile())
|
|
337
|
+
return false;
|
|
338
|
+
return (lstat.size === entry.size &&
|
|
339
|
+
lstat.mtimeMs === entry.mtimeMs &&
|
|
340
|
+
lstat.ctimeMs === entry.ctimeMs);
|
|
341
|
+
}
|
|
250
342
|
function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat) {
|
|
251
343
|
const items = [];
|
|
252
344
|
for (const entry of filesToShare) {
|
|
@@ -287,10 +379,37 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat) {
|
|
|
287
379
|
items.push({ action: "skip-size-limit", absolutePath, relativePath });
|
|
288
380
|
continue;
|
|
289
381
|
}
|
|
290
|
-
// Stat
|
|
291
|
-
//
|
|
292
|
-
//
|
|
382
|
+
// Stat fast path. Reading and hashing every regular file every pass costs
|
|
383
|
+
// one full read of the vault — hundreds of thousands of files on a real
|
|
384
|
+
// install — to answer a question stat can usually answer for free.
|
|
385
|
+
//
|
|
386
|
+
// The gate is size + mtime + ctime, and ctime is what makes it safe. mtime
|
|
387
|
+
// is forgeable: `cp -p`, `touch -r` and `rsync --times` put an old mtime
|
|
388
|
+
// back on new bytes, so a size+mtime check would skip such an edit forever
|
|
389
|
+
// — the failure this code previously hashed everything to avoid. ctime is
|
|
390
|
+
// the inode-change stamp and userspace cannot set it; the very utimes call
|
|
391
|
+
// that restores mtime moves ctime to now. A file that clears all three has
|
|
392
|
+
// not been written since we hashed it.
|
|
393
|
+
//
|
|
394
|
+
// Deliberately NOT skipped here:
|
|
395
|
+
// - entries with no recorded mtime/ctime (written before stat tracking,
|
|
396
|
+
// or by a downloader that did not stamp) — hashed once, then stamped
|
|
397
|
+
// by the restamp below so the next pass is free;
|
|
398
|
+
// - `localDiverges` entries — see the hash gate below for why equality
|
|
399
|
+
// proves nothing about them.
|
|
400
|
+
if (skipUnchanged && statMatchesJournal(absolutePath, journal.files[relativePath])) {
|
|
401
|
+
items.push({ action: "skip-unchanged", absolutePath, relativePath });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// Snapshot stat either side of the read. If the two differ the file was
|
|
405
|
+
// rewritten while we hashed it, so `localHash` may describe bytes that are
|
|
406
|
+
// already gone — and nothing downstream may stamp a timestamp against it.
|
|
407
|
+
const statBeforeHash = statSnapshot(absolutePath);
|
|
293
408
|
const localHash = hashFile(absolutePath);
|
|
409
|
+
const statAfterHash = statSnapshot(absolutePath);
|
|
410
|
+
const statAtHash = sameStatSnapshot(statBeforeHash, statAfterHash)
|
|
411
|
+
? statAfterHash ?? undefined
|
|
412
|
+
: undefined;
|
|
294
413
|
if (skipUnchanged) {
|
|
295
414
|
const existing = journal.files[relativePath];
|
|
296
415
|
// A `localDiverges` entry records the hash of the KEPT LOCAL body, so
|
|
@@ -308,15 +427,11 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat) {
|
|
|
308
427
|
// journal write on an already-current entry). Stat failure → no
|
|
309
428
|
// restamp; the skip still stands.
|
|
310
429
|
let restamp;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
/* best-effort; a stat error just forgoes the fast-path refresh */
|
|
430
|
+
if (statAtHash &&
|
|
431
|
+
(existing.mtimeMs !== statAtHash.mtimeMs ||
|
|
432
|
+
existing.ctimeMs !== statAtHash.ctimeMs ||
|
|
433
|
+
existing.size !== statAtHash.size)) {
|
|
434
|
+
restamp = statAtHash;
|
|
320
435
|
}
|
|
321
436
|
items.push({ action: "skip-unchanged", absolutePath, relativePath, restamp });
|
|
322
437
|
continue;
|
|
@@ -330,6 +445,7 @@ function computePushPlan(filesToShare, journal, skipUnchanged, onHeartbeat) {
|
|
|
330
445
|
relativePath,
|
|
331
446
|
localHash,
|
|
332
447
|
size,
|
|
448
|
+
...(statAtHash ? { statAtHash } : {}),
|
|
333
449
|
});
|
|
334
450
|
}
|
|
335
451
|
let filesToUpload = 0;
|
|
@@ -863,6 +979,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
863
979
|
const existing = run.journal.files[item.relativePath];
|
|
864
980
|
if (existing && existing.hash) {
|
|
865
981
|
existing.mtimeMs = item.restamp.mtimeMs;
|
|
982
|
+
existing.ctimeMs = item.restamp.ctimeMs;
|
|
866
983
|
existing.size = item.restamp.size;
|
|
867
984
|
}
|
|
868
985
|
}
|
|
@@ -979,6 +1096,7 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
979
1096
|
updateEntry(run.journal, relativePath, localHash, lstat.size, "up", absolutePath, {
|
|
980
1097
|
remoteEtag: remoteMeta.etag,
|
|
981
1098
|
mtimeMs: lstat.mtimeMs,
|
|
1099
|
+
ctimeMs: ctimeToStamp(item.kind === "file" ? item.statAtHash : undefined, lstat),
|
|
982
1100
|
kind: item.kind,
|
|
983
1101
|
});
|
|
984
1102
|
// Same checkpoint contract as an actual upload. After repeated
|
|
@@ -1071,10 +1189,11 @@ async function executeUploads(run, pushPlan, counters, conflictPaths) {
|
|
|
1071
1189
|
const lstat = fs.lstatSync(absolutePath);
|
|
1072
1190
|
const size = isSymlinkUpload ? 0 : lstat.size;
|
|
1073
1191
|
const mtimeMs = lstat.mtimeMs;
|
|
1192
|
+
const ctimeMs = ctimeToStamp(item.kind === "file" ? item.statAtHash : undefined, lstat);
|
|
1074
1193
|
const { etag } = isSymlinkUpload
|
|
1075
1194
|
? await uploadSymlink(run.ctx, item.target, relativePath, run.options.author, pc)
|
|
1076
1195
|
: await uploadFile(run.ctx, absolutePath, relativePath, run.options.author, pc);
|
|
1077
|
-
updateEntry(run.journal, relativePath, localHash, size, "up", absolutePath, { remoteEtag: etag, mtimeMs, kind: item.kind });
|
|
1196
|
+
updateEntry(run.journal, relativePath, localHash, size, "up", absolutePath, { remoteEtag: etag, mtimeMs, ctimeMs, kind: item.kind });
|
|
1078
1197
|
if (run.message) {
|
|
1079
1198
|
run.journal.files[relativePath] = {
|
|
1080
1199
|
...run.journal.files[relativePath],
|