@rljson/fs-agent 0.0.19 → 0.0.20
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.js +99 -67
- package/dist/fs-scanner.d.ts +13 -0
- package/package.json +1 -1
package/dist/fs-agent.js
CHANGED
|
@@ -441,6 +441,8 @@ class FsScanner {
|
|
|
441
441
|
_nextBlobCache = /* @__PURE__ */ new Map();
|
|
442
442
|
/** Ensures the persisted cache is read from disk only once. */
|
|
443
443
|
_cacheLoaded = false;
|
|
444
|
+
/** Entries that vanished mid-scan, counted per {@link scan} for one summary. */
|
|
445
|
+
_vanishedDuringScan = 0;
|
|
444
446
|
constructor(rootPath, options = {}) {
|
|
445
447
|
this._rootPath = rootPath;
|
|
446
448
|
this._options = {
|
|
@@ -485,6 +487,7 @@ class FsScanner {
|
|
|
485
487
|
this._nextBlobCache = /* @__PURE__ */ new Map();
|
|
486
488
|
}
|
|
487
489
|
const trees = /* @__PURE__ */ new Map();
|
|
490
|
+
this._vanishedDuringScan = 0;
|
|
488
491
|
let rootTree;
|
|
489
492
|
try {
|
|
490
493
|
rootTree = await this._scanDirectory(this._rootPath, ".", 0, trees);
|
|
@@ -500,6 +503,11 @@ class FsScanner {
|
|
|
500
503
|
"Failed to generate hash for root tree. Tree structure may be invalid."
|
|
501
504
|
);
|
|
502
505
|
}
|
|
506
|
+
if (this._vanishedDuringScan > 0) {
|
|
507
|
+
console.warn(
|
|
508
|
+
`[fs-scanner] ${this._vanishedDuringScan} entr${this._vanishedDuringScan === 1 ? "y" : "ies"} vanished during the scan of ${this._rootPath} — skipped; the next scan picks up the settled state`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
503
511
|
trees.set(rootHashStr, rootTree);
|
|
504
512
|
this._tree = {
|
|
505
513
|
rootHash: rootHashStr,
|
|
@@ -513,6 +521,7 @@ class FsScanner {
|
|
|
513
521
|
}
|
|
514
522
|
async _scanDirectory(absolutePath, relativePath, depth, trees) {
|
|
515
523
|
const entries = await readdir(absolutePath, { withFileTypes: true });
|
|
524
|
+
const childTrees = [];
|
|
516
525
|
const childRefs = [];
|
|
517
526
|
for (const entry of entries) {
|
|
518
527
|
if (this._shouldIgnore(entry.name)) {
|
|
@@ -526,78 +535,87 @@ class FsScanner {
|
|
|
526
535
|
if (this._options.maxDepth !== void 0 && depth >= this._options.maxDepth) {
|
|
527
536
|
continue;
|
|
528
537
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
fileContent
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
)
|
|
538
|
+
try {
|
|
539
|
+
const childStats = await stat(childPath);
|
|
540
|
+
if (entry.isDirectory()) {
|
|
541
|
+
const childTree = await this._scanDirectory(
|
|
542
|
+
childPath,
|
|
543
|
+
childRelPath,
|
|
544
|
+
depth + 1,
|
|
545
|
+
trees
|
|
546
|
+
);
|
|
547
|
+
childTrees.push(childTree);
|
|
548
|
+
hip(childTree);
|
|
549
|
+
const childHashStr = childTree._hash;
|
|
550
|
+
trees.set(childHashStr, childTree);
|
|
551
|
+
childRefs.push(childHashStr);
|
|
552
|
+
} else if (entry.isFile()) {
|
|
553
|
+
const mtimeMs = childStats.mtime.getTime();
|
|
554
|
+
const cached = this._scanCachePath ? this._blobCache.get(childRelPath) : void 0;
|
|
555
|
+
let blobId;
|
|
556
|
+
if (cached && cached.mtime === mtimeMs && cached.size === childStats.size) {
|
|
557
|
+
blobId = cached.blobId;
|
|
558
|
+
} else {
|
|
559
|
+
let fileContent;
|
|
560
|
+
try {
|
|
561
|
+
fileContent = await readFile(childPath);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (FsScanner._isVanished(error)) throw error;
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Failed to read file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
let blobProps;
|
|
569
|
+
try {
|
|
570
|
+
blobProps = await this._bs.setBlob(fileContent);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`Failed to store blob for file "${childRelPath}": ${error instanceof Error ? error.message : String(error)}`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (!blobProps || !blobProps.blobId) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`Blob storage returned invalid blobId for file "${childRelPath}"`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
blobId = blobProps.blobId;
|
|
563
582
|
}
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
583
|
+
if (this._scanCachePath) {
|
|
584
|
+
this._nextBlobCache.set(childRelPath, {
|
|
585
|
+
mtime: mtimeMs,
|
|
586
|
+
size: childStats.size,
|
|
587
|
+
blobId
|
|
588
|
+
});
|
|
568
589
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
mtime: mtimeMs,
|
|
590
|
+
const fileMeta = {
|
|
591
|
+
name: entry.name,
|
|
592
|
+
type: "file",
|
|
593
|
+
relativePath: childRelPath,
|
|
574
594
|
size: childStats.size,
|
|
595
|
+
// mtime is kept for files (restore preserves it, so it round-trips to
|
|
596
|
+
// the same ref on every client) but NOT for directories (a folder's
|
|
597
|
+
// mtime is per-machine and does not round-trip). The absolute `path`
|
|
598
|
+
// is excluded everywhere — it is folder-specific.
|
|
599
|
+
mtime: mtimeMs,
|
|
575
600
|
blobId
|
|
576
|
-
|
|
601
|
+
// Link to content in Bs
|
|
602
|
+
};
|
|
603
|
+
const fileTree = {
|
|
604
|
+
id: entry.name,
|
|
605
|
+
isParent: false,
|
|
606
|
+
meta: fileMeta,
|
|
607
|
+
children: null
|
|
608
|
+
};
|
|
609
|
+
childTrees.push(fileTree);
|
|
610
|
+
hip(fileTree);
|
|
611
|
+
const fileTreeHashStr = fileTree._hash;
|
|
612
|
+
trees.set(fileTreeHashStr, fileTree);
|
|
613
|
+
childRefs.push(fileTreeHashStr);
|
|
577
614
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
size: childStats.size,
|
|
583
|
-
// mtime is kept for files (restore preserves it, so it round-trips to
|
|
584
|
-
// the same ref on every client) but NOT for directories (a folder's
|
|
585
|
-
// mtime is per-machine and does not round-trip). The absolute `path`
|
|
586
|
-
// is excluded everywhere — it is folder-specific.
|
|
587
|
-
mtime: mtimeMs,
|
|
588
|
-
blobId
|
|
589
|
-
// Link to content in Bs
|
|
590
|
-
};
|
|
591
|
-
const fileTree = {
|
|
592
|
-
id: entry.name,
|
|
593
|
-
isParent: false,
|
|
594
|
-
meta: fileMeta,
|
|
595
|
-
children: null
|
|
596
|
-
};
|
|
597
|
-
hip(fileTree);
|
|
598
|
-
const fileTreeHashStr = fileTree._hash;
|
|
599
|
-
trees.set(fileTreeHashStr, fileTree);
|
|
600
|
-
childRefs.push(fileTreeHashStr);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (!FsScanner._isVanished(error)) throw error;
|
|
617
|
+
this._vanishedDuringScan++;
|
|
618
|
+
continue;
|
|
601
619
|
}
|
|
602
620
|
}
|
|
603
621
|
const dirName = relativePath === "." ? "." : (
|
|
@@ -766,6 +784,20 @@ class FsScanner {
|
|
|
766
784
|
static _errMessage(err) {
|
|
767
785
|
return err instanceof Error ? err.message : String(err);
|
|
768
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* Whether a caught value is a "no longer there" filesystem error.
|
|
789
|
+
*
|
|
790
|
+
* A scan walks a directory that is still being written to. `readdir` returns
|
|
791
|
+
* a name, and by the time the entry is `stat`ed, read, or descended into it
|
|
792
|
+
* can be gone — a save-and-rename editor, a build tool, a peer applying a
|
|
793
|
+
* deletion. That is ordinary, not exceptional.
|
|
794
|
+
* @param err - The caught value.
|
|
795
|
+
* @returns `true` for ENOENT / ENOTDIR.
|
|
796
|
+
*/
|
|
797
|
+
static _isVanished(err) {
|
|
798
|
+
const code = err?.code;
|
|
799
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
800
|
+
}
|
|
769
801
|
/** Whether the host is Windows — gates Windows-specific watcher hardening. */
|
|
770
802
|
static get _isWindows() {
|
|
771
803
|
return process.platform === "win32";
|
package/dist/fs-scanner.d.ts
CHANGED
|
@@ -117,6 +117,8 @@ export declare class FsScanner {
|
|
|
117
117
|
private _nextBlobCache;
|
|
118
118
|
/** Ensures the persisted cache is read from disk only once. */
|
|
119
119
|
private _cacheLoaded;
|
|
120
|
+
/** Entries that vanished mid-scan, counted per {@link scan} for one summary. */
|
|
121
|
+
private _vanishedDuringScan;
|
|
120
122
|
constructor(rootPath: string, options?: FsScanOptions);
|
|
121
123
|
get tree(): FsTree | null;
|
|
122
124
|
get rootPath(): string;
|
|
@@ -156,6 +158,17 @@ export declare class FsScanner {
|
|
|
156
158
|
* @returns A message string
|
|
157
159
|
*/
|
|
158
160
|
private static _errMessage;
|
|
161
|
+
/**
|
|
162
|
+
* Whether a caught value is a "no longer there" filesystem error.
|
|
163
|
+
*
|
|
164
|
+
* A scan walks a directory that is still being written to. `readdir` returns
|
|
165
|
+
* a name, and by the time the entry is `stat`ed, read, or descended into it
|
|
166
|
+
* can be gone — a save-and-rename editor, a build tool, a peer applying a
|
|
167
|
+
* deletion. That is ordinary, not exceptional.
|
|
168
|
+
* @param err - The caught value.
|
|
169
|
+
* @returns `true` for ENOENT / ENOTDIR.
|
|
170
|
+
*/
|
|
171
|
+
private static _isVanished;
|
|
159
172
|
/** Whether the host is Windows — gates Windows-specific watcher hardening. */
|
|
160
173
|
private static get _isWindows();
|
|
161
174
|
private _handleFileChange;
|