@rljson/fs-agent 0.0.20 → 0.0.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/fs-agent.d.ts +48 -0
- package/dist/fs-agent.js +108 -4
- package/package.json +1 -1
package/dist/fs-agent.d.ts
CHANGED
|
@@ -159,6 +159,14 @@ export declare class FsAgent {
|
|
|
159
159
|
* view — and undo a deletion the peer just made.
|
|
160
160
|
*/
|
|
161
161
|
private _remoteApplyInFlight;
|
|
162
|
+
/** Files written vs left alone by the current {@link restore}. */
|
|
163
|
+
private _restoreWritten;
|
|
164
|
+
private _restoreSkipped;
|
|
165
|
+
/**
|
|
166
|
+
* What this agent last wrote to each absolute path, so a repeat restore can
|
|
167
|
+
* recognise its own work without re-reading the file.
|
|
168
|
+
*/
|
|
169
|
+
private _restoredBlobs;
|
|
162
170
|
private _timeouts;
|
|
163
171
|
/** Client-only: resolve DAG-branch conflicts into merge revisions. */
|
|
164
172
|
private _resolveConflicts;
|
|
@@ -292,8 +300,48 @@ export declare class FsAgent {
|
|
|
292
300
|
* @param treeHash - Hash of the tree node to restore
|
|
293
301
|
* @param trees - Map of all tree nodes
|
|
294
302
|
* @param targetPath - Target directory path
|
|
303
|
+
* @param isOwnRoot - Whether `targetPath` is this agent's own folder, which
|
|
304
|
+
* is the only case where the scanner's view describes these files
|
|
295
305
|
*/
|
|
296
306
|
private _restoreTree;
|
|
307
|
+
/**
|
|
308
|
+
* The content identity this agent believes is on disk at `filePath`, or
|
|
309
|
+
* `undefined` when it has no basis for an opinion.
|
|
310
|
+
*
|
|
311
|
+
* Two sources, both anchored on a real blobId rather than a guess:
|
|
312
|
+
* what this agent last wrote there, and what the scanner hashed at its last
|
|
313
|
+
* scan of the folder (which survives a restart, so a fresh process still
|
|
314
|
+
* skips an unchanged 80 GB catalogue).
|
|
315
|
+
* @param filePath - Absolute path of the file.
|
|
316
|
+
* @param relativePath - Its path within the tree.
|
|
317
|
+
* @param isOwnRoot - Whether the restore target is this agent's own folder,
|
|
318
|
+
* which is the only case where the scanner's view describes this file.
|
|
319
|
+
* @returns The believed content identity, or `undefined`.
|
|
320
|
+
*/
|
|
321
|
+
private _knownOnDisk;
|
|
322
|
+
/**
|
|
323
|
+
* Whether the file at `filePath` is already the content `meta` describes.
|
|
324
|
+
*
|
|
325
|
+
* The decision is anchored on the blobId: a different blobId is always
|
|
326
|
+
* rewritten, whatever the timestamps say. Hashing the file instead would
|
|
327
|
+
* mean reading 80 GB to avoid writing 80 GB, which saves nothing — so the
|
|
328
|
+
* known blobId is verified against a `stat`, which catches a file edited
|
|
329
|
+
* since this agent last had an opinion about it.
|
|
330
|
+
*
|
|
331
|
+
* Deliberately one-directional in its uncertainty: every unclear case
|
|
332
|
+
* answers `false` and the file is rewritten. A needless write costs time; a
|
|
333
|
+
* wrongly skipped write leaves the wrong bytes on disk indefinitely.
|
|
334
|
+
*
|
|
335
|
+
* Anchoring on the blobId is not belt-and-braces. Size and mtime alone
|
|
336
|
+
* cannot see a same-size edit made inside the same millisecond — the scan
|
|
337
|
+
* cache tolerates that, but a restore must not: there the cost is not a
|
|
338
|
+
* stale cache entry, it is the wrong file contents left in place.
|
|
339
|
+
* @param filePath - Absolute path of the file to check.
|
|
340
|
+
* @param meta - The metadata describing the content that should be there.
|
|
341
|
+
* @param isOwnRoot - Whether the target is this agent's own folder.
|
|
342
|
+
* @returns `true` only when the file is certainly already correct.
|
|
343
|
+
*/
|
|
344
|
+
private _alreadyOnDisk;
|
|
297
345
|
/**
|
|
298
346
|
* Gets the current tree structure
|
|
299
347
|
*/
|
package/dist/fs-agent.js
CHANGED
|
@@ -505,7 +505,13 @@ class FsScanner {
|
|
|
505
505
|
}
|
|
506
506
|
if (this._vanishedDuringScan > 0) {
|
|
507
507
|
console.warn(
|
|
508
|
-
`[fs-scanner] ${this._vanishedDuringScan} entr${this._vanishedDuringScan === 1 ? "y" : "ies"} vanished during the scan of ${this._rootPath} —
|
|
508
|
+
`[fs-scanner] ${this._vanishedDuringScan} entr${this._vanishedDuringScan === 1 ? "y" : "ies"} vanished during the scan of ${this._rootPath} — this scan is a partial picture`
|
|
509
|
+
);
|
|
510
|
+
if (this._tree) {
|
|
511
|
+
return this._tree;
|
|
512
|
+
}
|
|
513
|
+
console.warn(
|
|
514
|
+
`[fs-scanner] no previous scan of ${this._rootPath} to fall back on — starting from the partial one`
|
|
509
515
|
);
|
|
510
516
|
}
|
|
511
517
|
trees.set(rootHashStr, rootTree);
|
|
@@ -1044,6 +1050,14 @@ class FsAgent {
|
|
|
1044
1050
|
* view — and undo a deletion the peer just made.
|
|
1045
1051
|
*/
|
|
1046
1052
|
_remoteApplyInFlight = false;
|
|
1053
|
+
/** Files written vs left alone by the current {@link restore}. */
|
|
1054
|
+
_restoreWritten = 0;
|
|
1055
|
+
_restoreSkipped = 0;
|
|
1056
|
+
/**
|
|
1057
|
+
* What this agent last wrote to each absolute path, so a repeat restore can
|
|
1058
|
+
* recognise its own work without re-reading the file.
|
|
1059
|
+
*/
|
|
1060
|
+
_restoredBlobs = /* @__PURE__ */ new Map();
|
|
1047
1061
|
_timeouts;
|
|
1048
1062
|
/** Client-only: resolve DAG-branch conflicts into merge revisions. */
|
|
1049
1063
|
_resolveConflicts;
|
|
@@ -1307,7 +1321,19 @@ ${err.stack}` : String(err);
|
|
|
1307
1321
|
target
|
|
1308
1322
|
);
|
|
1309
1323
|
const preRestore = options?.cleanTarget ? await this._collectAllFiles(target) : /* @__PURE__ */ new Set();
|
|
1310
|
-
|
|
1324
|
+
this._restoreWritten = 0;
|
|
1325
|
+
this._restoreSkipped = 0;
|
|
1326
|
+
await this._restoreTree(
|
|
1327
|
+
tree.rootHash,
|
|
1328
|
+
tree.trees,
|
|
1329
|
+
target,
|
|
1330
|
+
target === this._rootPath
|
|
1331
|
+
);
|
|
1332
|
+
if (this._restoreSkipped > 0) {
|
|
1333
|
+
console.log(
|
|
1334
|
+
`[FsAgent] restore: wrote ${this._restoreWritten}, left ${this._restoreSkipped} already-correct file${this._restoreSkipped === 1 ? "" : "s"} untouched`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1311
1337
|
if (options?.cleanTarget) {
|
|
1312
1338
|
await this._pruneExtraneous(
|
|
1313
1339
|
target,
|
|
@@ -1347,8 +1373,10 @@ ${err.stack}` : String(err);
|
|
|
1347
1373
|
* @param treeHash - Hash of the tree node to restore
|
|
1348
1374
|
* @param trees - Map of all tree nodes
|
|
1349
1375
|
* @param targetPath - Target directory path
|
|
1376
|
+
* @param isOwnRoot - Whether `targetPath` is this agent's own folder, which
|
|
1377
|
+
* is the only case where the scanner's view describes these files
|
|
1350
1378
|
*/
|
|
1351
|
-
async _restoreTree(treeHash, trees, targetPath) {
|
|
1379
|
+
async _restoreTree(treeHash, trees, targetPath, isOwnRoot) {
|
|
1352
1380
|
const treeNode = trees.get(treeHash);
|
|
1353
1381
|
if (!treeNode) {
|
|
1354
1382
|
throw new Error(`Tree node not found: ${treeHash}`);
|
|
@@ -1360,6 +1388,10 @@ ${err.stack}` : String(err);
|
|
|
1360
1388
|
if (meta.type === "file") {
|
|
1361
1389
|
const filePath = join(targetPath, meta.relativePath);
|
|
1362
1390
|
if (meta.blobId) {
|
|
1391
|
+
if (await this._alreadyOnDisk(filePath, meta, isOwnRoot)) {
|
|
1392
|
+
this._restoreSkipped++;
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1363
1395
|
let fileBlob;
|
|
1364
1396
|
try {
|
|
1365
1397
|
fileBlob = await this._bs.getBlob(meta.blobId);
|
|
@@ -1375,21 +1407,93 @@ ${err.stack}` : String(err);
|
|
|
1375
1407
|
}
|
|
1376
1408
|
await mkdir(dirname(filePath), { recursive: true });
|
|
1377
1409
|
await FsAgent._atomicWriteFile(filePath, fileBlob.content);
|
|
1410
|
+
this._restoreWritten++;
|
|
1378
1411
|
if (meta.mtime) {
|
|
1379
1412
|
const mtime = new Date(meta.mtime);
|
|
1380
1413
|
await utimes(filePath, mtime, mtime);
|
|
1381
1414
|
}
|
|
1415
|
+
if (meta.size !== void 0 && meta.mtime !== void 0) {
|
|
1416
|
+
this._restoredBlobs.set(filePath, {
|
|
1417
|
+
blobId: meta.blobId,
|
|
1418
|
+
size: meta.size,
|
|
1419
|
+
mtime: meta.mtime
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1382
1422
|
}
|
|
1383
1423
|
} else if (meta.type === "directory") {
|
|
1384
1424
|
const dirPath = meta.relativePath === "." ? targetPath : join(targetPath, meta.relativePath);
|
|
1385
1425
|
await mkdir(dirPath, { recursive: true });
|
|
1386
1426
|
if (treeNode.children && Array.isArray(treeNode.children)) {
|
|
1387
1427
|
for (const childHash of treeNode.children) {
|
|
1388
|
-
await this._restoreTree(childHash, trees, targetPath);
|
|
1428
|
+
await this._restoreTree(childHash, trees, targetPath, isOwnRoot);
|
|
1389
1429
|
}
|
|
1390
1430
|
}
|
|
1391
1431
|
}
|
|
1392
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* The content identity this agent believes is on disk at `filePath`, or
|
|
1435
|
+
* `undefined` when it has no basis for an opinion.
|
|
1436
|
+
*
|
|
1437
|
+
* Two sources, both anchored on a real blobId rather than a guess:
|
|
1438
|
+
* what this agent last wrote there, and what the scanner hashed at its last
|
|
1439
|
+
* scan of the folder (which survives a restart, so a fresh process still
|
|
1440
|
+
* skips an unchanged 80 GB catalogue).
|
|
1441
|
+
* @param filePath - Absolute path of the file.
|
|
1442
|
+
* @param relativePath - Its path within the tree.
|
|
1443
|
+
* @param isOwnRoot - Whether the restore target is this agent's own folder,
|
|
1444
|
+
* which is the only case where the scanner's view describes this file.
|
|
1445
|
+
* @returns The believed content identity, or `undefined`.
|
|
1446
|
+
*/
|
|
1447
|
+
_knownOnDisk(filePath, relativePath, isOwnRoot) {
|
|
1448
|
+
const written = this._restoredBlobs.get(filePath);
|
|
1449
|
+
if (written) return written;
|
|
1450
|
+
if (!isOwnRoot) return void 0;
|
|
1451
|
+
const scanned = this._scanner.getTreeByPath(relativePath)?.meta;
|
|
1452
|
+
if (scanned?.blobId === void 0 || scanned.size === void 0 || scanned.mtime === void 0) {
|
|
1453
|
+
return void 0;
|
|
1454
|
+
}
|
|
1455
|
+
return {
|
|
1456
|
+
blobId: scanned.blobId,
|
|
1457
|
+
size: scanned.size,
|
|
1458
|
+
mtime: scanned.mtime
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Whether the file at `filePath` is already the content `meta` describes.
|
|
1463
|
+
*
|
|
1464
|
+
* The decision is anchored on the blobId: a different blobId is always
|
|
1465
|
+
* rewritten, whatever the timestamps say. Hashing the file instead would
|
|
1466
|
+
* mean reading 80 GB to avoid writing 80 GB, which saves nothing — so the
|
|
1467
|
+
* known blobId is verified against a `stat`, which catches a file edited
|
|
1468
|
+
* since this agent last had an opinion about it.
|
|
1469
|
+
*
|
|
1470
|
+
* Deliberately one-directional in its uncertainty: every unclear case
|
|
1471
|
+
* answers `false` and the file is rewritten. A needless write costs time; a
|
|
1472
|
+
* wrongly skipped write leaves the wrong bytes on disk indefinitely.
|
|
1473
|
+
*
|
|
1474
|
+
* Anchoring on the blobId is not belt-and-braces. Size and mtime alone
|
|
1475
|
+
* cannot see a same-size edit made inside the same millisecond — the scan
|
|
1476
|
+
* cache tolerates that, but a restore must not: there the cost is not a
|
|
1477
|
+
* stale cache entry, it is the wrong file contents left in place.
|
|
1478
|
+
* @param filePath - Absolute path of the file to check.
|
|
1479
|
+
* @param meta - The metadata describing the content that should be there.
|
|
1480
|
+
* @param isOwnRoot - Whether the target is this agent's own folder.
|
|
1481
|
+
* @returns `true` only when the file is certainly already correct.
|
|
1482
|
+
*/
|
|
1483
|
+
async _alreadyOnDisk(filePath, meta, isOwnRoot) {
|
|
1484
|
+
const known = this._knownOnDisk(
|
|
1485
|
+
filePath,
|
|
1486
|
+
meta.relativePath,
|
|
1487
|
+
isOwnRoot
|
|
1488
|
+
);
|
|
1489
|
+
if (!known || known.blobId !== meta.blobId) return false;
|
|
1490
|
+
try {
|
|
1491
|
+
const st = await stat(filePath);
|
|
1492
|
+
return st.size === known.size && Math.abs(st.mtimeMs - known.mtime) < 1;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1393
1497
|
/**
|
|
1394
1498
|
* Gets the current tree structure
|
|
1395
1499
|
*/
|