@rljson/fs-agent 0.0.21 → 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 +101 -3
- 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
|
@@ -1050,6 +1050,14 @@ class FsAgent {
|
|
|
1050
1050
|
* view — and undo a deletion the peer just made.
|
|
1051
1051
|
*/
|
|
1052
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();
|
|
1053
1061
|
_timeouts;
|
|
1054
1062
|
/** Client-only: resolve DAG-branch conflicts into merge revisions. */
|
|
1055
1063
|
_resolveConflicts;
|
|
@@ -1313,7 +1321,19 @@ ${err.stack}` : String(err);
|
|
|
1313
1321
|
target
|
|
1314
1322
|
);
|
|
1315
1323
|
const preRestore = options?.cleanTarget ? await this._collectAllFiles(target) : /* @__PURE__ */ new Set();
|
|
1316
|
-
|
|
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
|
+
}
|
|
1317
1337
|
if (options?.cleanTarget) {
|
|
1318
1338
|
await this._pruneExtraneous(
|
|
1319
1339
|
target,
|
|
@@ -1353,8 +1373,10 @@ ${err.stack}` : String(err);
|
|
|
1353
1373
|
* @param treeHash - Hash of the tree node to restore
|
|
1354
1374
|
* @param trees - Map of all tree nodes
|
|
1355
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
|
|
1356
1378
|
*/
|
|
1357
|
-
async _restoreTree(treeHash, trees, targetPath) {
|
|
1379
|
+
async _restoreTree(treeHash, trees, targetPath, isOwnRoot) {
|
|
1358
1380
|
const treeNode = trees.get(treeHash);
|
|
1359
1381
|
if (!treeNode) {
|
|
1360
1382
|
throw new Error(`Tree node not found: ${treeHash}`);
|
|
@@ -1366,6 +1388,10 @@ ${err.stack}` : String(err);
|
|
|
1366
1388
|
if (meta.type === "file") {
|
|
1367
1389
|
const filePath = join(targetPath, meta.relativePath);
|
|
1368
1390
|
if (meta.blobId) {
|
|
1391
|
+
if (await this._alreadyOnDisk(filePath, meta, isOwnRoot)) {
|
|
1392
|
+
this._restoreSkipped++;
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1369
1395
|
let fileBlob;
|
|
1370
1396
|
try {
|
|
1371
1397
|
fileBlob = await this._bs.getBlob(meta.blobId);
|
|
@@ -1381,21 +1407,93 @@ ${err.stack}` : String(err);
|
|
|
1381
1407
|
}
|
|
1382
1408
|
await mkdir(dirname(filePath), { recursive: true });
|
|
1383
1409
|
await FsAgent._atomicWriteFile(filePath, fileBlob.content);
|
|
1410
|
+
this._restoreWritten++;
|
|
1384
1411
|
if (meta.mtime) {
|
|
1385
1412
|
const mtime = new Date(meta.mtime);
|
|
1386
1413
|
await utimes(filePath, mtime, mtime);
|
|
1387
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
|
+
}
|
|
1388
1422
|
}
|
|
1389
1423
|
} else if (meta.type === "directory") {
|
|
1390
1424
|
const dirPath = meta.relativePath === "." ? targetPath : join(targetPath, meta.relativePath);
|
|
1391
1425
|
await mkdir(dirPath, { recursive: true });
|
|
1392
1426
|
if (treeNode.children && Array.isArray(treeNode.children)) {
|
|
1393
1427
|
for (const childHash of treeNode.children) {
|
|
1394
|
-
await this._restoreTree(childHash, trees, targetPath);
|
|
1428
|
+
await this._restoreTree(childHash, trees, targetPath, isOwnRoot);
|
|
1395
1429
|
}
|
|
1396
1430
|
}
|
|
1397
1431
|
}
|
|
1398
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
|
+
}
|
|
1399
1497
|
/**
|
|
1400
1498
|
* Gets the current tree structure
|
|
1401
1499
|
*/
|