@componentor/fs 3.0.9 → 3.0.11
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/README.md +107 -10
- package/dist/index.js +227 -188
- package/dist/index.js.map +1 -1
- package/dist/workers/repair.worker.js +1678 -0
- package/dist/workers/repair.worker.js.map +1 -0
- package/dist/workers/server.worker.js +34 -6
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +828 -14
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1304,10 +1304,14 @@ var VFSFileSystem = class {
|
|
|
1304
1304
|
// Ready promise for async callers
|
|
1305
1305
|
readyPromise;
|
|
1306
1306
|
resolveReady;
|
|
1307
|
+
rejectReady;
|
|
1308
|
+
initError = null;
|
|
1307
1309
|
isReady = false;
|
|
1308
1310
|
// Config (definite assignment — always set when constructor doesn't return singleton)
|
|
1309
1311
|
config;
|
|
1310
1312
|
tabId;
|
|
1313
|
+
_mode;
|
|
1314
|
+
corruptionError = null;
|
|
1311
1315
|
/** Namespace string derived from root — used for lock names, BroadcastChannel, and SW scope
|
|
1312
1316
|
* so multiple VFS instances with different roots don't collide. */
|
|
1313
1317
|
ns;
|
|
@@ -1327,9 +1331,12 @@ var VFSFileSystem = class {
|
|
|
1327
1331
|
const ns = `vfs-${root.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
1328
1332
|
const existing = instanceRegistry.get(ns);
|
|
1329
1333
|
if (existing) return existing;
|
|
1334
|
+
const mode = config.mode ?? "hybrid";
|
|
1335
|
+
this._mode = mode;
|
|
1336
|
+
const opfsSync = config.opfsSync ?? mode === "hybrid";
|
|
1330
1337
|
this.config = {
|
|
1331
1338
|
root,
|
|
1332
|
-
opfsSync
|
|
1339
|
+
opfsSync,
|
|
1333
1340
|
opfsSyncRoot: config.opfsSyncRoot,
|
|
1334
1341
|
uid: config.uid ?? 0,
|
|
1335
1342
|
gid: config.gid ?? 0,
|
|
@@ -1341,8 +1348,9 @@ var VFSFileSystem = class {
|
|
|
1341
1348
|
};
|
|
1342
1349
|
this.tabId = crypto.randomUUID();
|
|
1343
1350
|
this.ns = ns;
|
|
1344
|
-
this.readyPromise = new Promise((resolve2) => {
|
|
1351
|
+
this.readyPromise = new Promise((resolve2, reject) => {
|
|
1345
1352
|
this.resolveReady = resolve2;
|
|
1353
|
+
this.rejectReady = reject;
|
|
1346
1354
|
});
|
|
1347
1355
|
this.promises = new VFSPromises(this._async, ns);
|
|
1348
1356
|
instanceRegistry.set(ns, this);
|
|
@@ -1369,7 +1377,9 @@ var VFSFileSystem = class {
|
|
|
1369
1377
|
this.initLeaderBroker();
|
|
1370
1378
|
}
|
|
1371
1379
|
} else if (msg.type === "init-failed") {
|
|
1372
|
-
if (
|
|
1380
|
+
if (msg.error?.startsWith("Corrupt VFS:")) {
|
|
1381
|
+
this.handleCorruptVFS(msg.error);
|
|
1382
|
+
} else if (this.holdingLeaderLock) {
|
|
1373
1383
|
setTimeout(() => this.sendLeaderInit(), 500);
|
|
1374
1384
|
} else if (!("locks" in navigator)) {
|
|
1375
1385
|
this.startAsFollower();
|
|
@@ -1460,10 +1470,50 @@ var VFSFileSystem = class {
|
|
|
1460
1470
|
}
|
|
1461
1471
|
});
|
|
1462
1472
|
}
|
|
1473
|
+
/** Send init-opfs message to sync-relay for OPFS-direct mode */
|
|
1474
|
+
sendOPFSInit() {
|
|
1475
|
+
this.syncWorker.postMessage({
|
|
1476
|
+
type: "init-opfs",
|
|
1477
|
+
sab: this.hasSAB ? this.sab : null,
|
|
1478
|
+
readySab: this.hasSAB ? this.readySab : null,
|
|
1479
|
+
asyncSab: this.hasSAB ? this.asyncSab : null,
|
|
1480
|
+
tabId: this.tabId,
|
|
1481
|
+
config: {
|
|
1482
|
+
root: this.config.root,
|
|
1483
|
+
ns: this.ns,
|
|
1484
|
+
uid: this.config.uid,
|
|
1485
|
+
gid: this.config.gid,
|
|
1486
|
+
debug: this.config.debug
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
/** Handle VFS corruption: log error, fall back to OPFS-direct mode.
|
|
1491
|
+
* The readyPromise will resolve once OPFS mode is ready, but init()
|
|
1492
|
+
* will reject with the corruption error to inform the caller. */
|
|
1493
|
+
handleCorruptVFS(errorMessage) {
|
|
1494
|
+
const err = new Error(`${errorMessage} \u2014 Falling back to OPFS mode`);
|
|
1495
|
+
this.corruptionError = err;
|
|
1496
|
+
console.error(`[VFS] ${err.message}`);
|
|
1497
|
+
if (this._mode === "vfs") {
|
|
1498
|
+
this.initError = err;
|
|
1499
|
+
this.rejectReady(err);
|
|
1500
|
+
if (this.hasSAB) {
|
|
1501
|
+
Atomics.store(this.readySignal, 0, -1);
|
|
1502
|
+
Atomics.notify(this.readySignal, 0);
|
|
1503
|
+
}
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
this._mode = "opfs";
|
|
1507
|
+
this.sendOPFSInit();
|
|
1508
|
+
}
|
|
1463
1509
|
/** Start as leader — tell sync-relay to init VFS engine + OPFS handle */
|
|
1464
1510
|
startAsLeader() {
|
|
1465
1511
|
this.isFollower = false;
|
|
1466
|
-
this.
|
|
1512
|
+
if (this._mode === "opfs") {
|
|
1513
|
+
this.sendOPFSInit();
|
|
1514
|
+
} else {
|
|
1515
|
+
this.sendLeaderInit();
|
|
1516
|
+
}
|
|
1467
1517
|
}
|
|
1468
1518
|
/** Start as follower — connect to leader via service worker port brokering */
|
|
1469
1519
|
startAsFollower() {
|
|
@@ -1564,8 +1614,9 @@ var VFSFileSystem = class {
|
|
|
1564
1614
|
this.leaderChangeBc.close();
|
|
1565
1615
|
this.leaderChangeBc = null;
|
|
1566
1616
|
}
|
|
1567
|
-
this.readyPromise = new Promise((resolve2) => {
|
|
1617
|
+
this.readyPromise = new Promise((resolve2, reject) => {
|
|
1568
1618
|
this.resolveReady = resolve2;
|
|
1619
|
+
this.rejectReady = reject;
|
|
1569
1620
|
});
|
|
1570
1621
|
this.syncWorker.terminate();
|
|
1571
1622
|
this.asyncWorker.terminate();
|
|
@@ -1586,8 +1637,12 @@ var VFSFileSystem = class {
|
|
|
1586
1637
|
this.resolveReady();
|
|
1587
1638
|
this.initLeaderBroker();
|
|
1588
1639
|
} else if (msg.type === "init-failed") {
|
|
1589
|
-
|
|
1590
|
-
|
|
1640
|
+
if (msg.error?.startsWith("Corrupt VFS:")) {
|
|
1641
|
+
this.handleCorruptVFS(msg.error);
|
|
1642
|
+
} else {
|
|
1643
|
+
console.warn("[VFS] Promotion: OPFS handle still busy, retrying...");
|
|
1644
|
+
setTimeout(() => this.sendLeaderInit(), 500);
|
|
1645
|
+
}
|
|
1591
1646
|
}
|
|
1592
1647
|
};
|
|
1593
1648
|
this.asyncWorker.onmessage = (e) => {
|
|
@@ -1617,7 +1672,11 @@ var VFSFileSystem = class {
|
|
|
1617
1672
|
[mc.port2]
|
|
1618
1673
|
);
|
|
1619
1674
|
}
|
|
1620
|
-
this.
|
|
1675
|
+
if (this._mode === "opfs") {
|
|
1676
|
+
this.sendOPFSInit();
|
|
1677
|
+
} else {
|
|
1678
|
+
this.sendLeaderInit();
|
|
1679
|
+
}
|
|
1621
1680
|
}
|
|
1622
1681
|
/** Spawn an inline worker from bundled code */
|
|
1623
1682
|
spawnWorker(name) {
|
|
@@ -1628,14 +1687,23 @@ var VFSFileSystem = class {
|
|
|
1628
1687
|
/** Block until workers are ready */
|
|
1629
1688
|
ensureReady() {
|
|
1630
1689
|
if (this.isReady) return;
|
|
1690
|
+
if (this.initError) throw this.initError;
|
|
1631
1691
|
if (!this.hasSAB) {
|
|
1632
1692
|
throw new Error("Sync API requires crossOriginIsolated (COOP/COEP headers). Use the promises API instead.");
|
|
1633
1693
|
}
|
|
1634
|
-
|
|
1694
|
+
const signal = Atomics.load(this.readySignal, 0);
|
|
1695
|
+
if (signal === 1) {
|
|
1635
1696
|
this.isReady = true;
|
|
1636
1697
|
return;
|
|
1637
1698
|
}
|
|
1699
|
+
if (signal === -1) {
|
|
1700
|
+
throw this.initError ?? new Error("VFS initialization failed");
|
|
1701
|
+
}
|
|
1638
1702
|
spinWait(this.readySignal, 0, 0);
|
|
1703
|
+
const finalSignal = Atomics.load(this.readySignal, 0);
|
|
1704
|
+
if (finalSignal === -1) {
|
|
1705
|
+
throw this.initError ?? new Error("VFS initialization failed");
|
|
1706
|
+
}
|
|
1639
1707
|
this.isReady = true;
|
|
1640
1708
|
}
|
|
1641
1709
|
/** Send a sync request via SAB and wait for response */
|
|
@@ -1886,8 +1954,101 @@ var VFSFileSystem = class {
|
|
|
1886
1954
|
}
|
|
1887
1955
|
purgeSync() {
|
|
1888
1956
|
}
|
|
1889
|
-
/**
|
|
1957
|
+
/** The current filesystem mode. Changes to 'opfs' on corruption fallback. */
|
|
1958
|
+
get mode() {
|
|
1959
|
+
return this._mode;
|
|
1960
|
+
}
|
|
1961
|
+
/** Async init helper — avoid blocking main thread.
|
|
1962
|
+
* Rejects with corruption error if VFS was corrupt (but system falls back to OPFS mode).
|
|
1963
|
+
* Callers can catch and continue — the fs API works in OPFS mode after rejection. */
|
|
1890
1964
|
init() {
|
|
1965
|
+
return this.readyPromise.then(() => {
|
|
1966
|
+
if (this.corruptionError) {
|
|
1967
|
+
throw this.corruptionError;
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/** Switch the filesystem mode at runtime.
|
|
1972
|
+
*
|
|
1973
|
+
* Typical flow for IDE corruption recovery:
|
|
1974
|
+
* 1. `await fs.init()` throws with corruption error (auto-falls back to opfs)
|
|
1975
|
+
* 2. IDE shows warning, user clicks "Repair" → call `repairVFS(root, fs)`
|
|
1976
|
+
* 3. After repair: `await fs.setMode('hybrid')` to resume normal VFS+OPFS mode
|
|
1977
|
+
*
|
|
1978
|
+
* Returns a Promise that resolves when the new mode is ready. */
|
|
1979
|
+
async setMode(newMode) {
|
|
1980
|
+
if (newMode === this._mode && this.isReady && !this.corruptionError) {
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
this._mode = newMode;
|
|
1984
|
+
this.corruptionError = null;
|
|
1985
|
+
this.initError = null;
|
|
1986
|
+
this.isReady = false;
|
|
1987
|
+
this.config.opfsSync = newMode === "hybrid";
|
|
1988
|
+
this.readyPromise = new Promise((resolve2, reject) => {
|
|
1989
|
+
this.resolveReady = resolve2;
|
|
1990
|
+
this.rejectReady = reject;
|
|
1991
|
+
});
|
|
1992
|
+
this.syncWorker.terminate();
|
|
1993
|
+
this.asyncWorker.terminate();
|
|
1994
|
+
const sabSize = this.config.sabSize;
|
|
1995
|
+
if (this.hasSAB) {
|
|
1996
|
+
this.sab = new SharedArrayBuffer(sabSize);
|
|
1997
|
+
this.readySab = new SharedArrayBuffer(4);
|
|
1998
|
+
this.asyncSab = new SharedArrayBuffer(sabSize);
|
|
1999
|
+
this.ctrl = new Int32Array(this.sab, 0, 8);
|
|
2000
|
+
this.readySignal = new Int32Array(this.readySab, 0, 1);
|
|
2001
|
+
}
|
|
2002
|
+
this.syncWorker = this.spawnWorker("sync-relay");
|
|
2003
|
+
this.asyncWorker = this.spawnWorker("async-relay");
|
|
2004
|
+
this.syncWorker.onmessage = (e) => {
|
|
2005
|
+
const msg = e.data;
|
|
2006
|
+
if (msg.type === "ready") {
|
|
2007
|
+
this.isReady = true;
|
|
2008
|
+
this.resolveReady();
|
|
2009
|
+
if (!this.isFollower) {
|
|
2010
|
+
this.initLeaderBroker();
|
|
2011
|
+
}
|
|
2012
|
+
} else if (msg.type === "init-failed") {
|
|
2013
|
+
if (msg.error?.startsWith("Corrupt VFS:")) {
|
|
2014
|
+
this.handleCorruptVFS(msg.error);
|
|
2015
|
+
} else if (this.holdingLeaderLock) {
|
|
2016
|
+
setTimeout(() => this.sendLeaderInit(), 500);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
this.asyncWorker.onmessage = (e) => {
|
|
2021
|
+
const msg = e.data;
|
|
2022
|
+
if (msg.type === "response") {
|
|
2023
|
+
const pending = this.asyncPending.get(msg.callId);
|
|
2024
|
+
if (pending) {
|
|
2025
|
+
this.asyncPending.delete(msg.callId);
|
|
2026
|
+
pending.resolve({ status: msg.status, data: msg.data });
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
if (this.hasSAB) {
|
|
2031
|
+
this.asyncWorker.postMessage({
|
|
2032
|
+
type: "init-leader",
|
|
2033
|
+
asyncSab: this.asyncSab,
|
|
2034
|
+
wakeSab: this.sab
|
|
2035
|
+
});
|
|
2036
|
+
} else {
|
|
2037
|
+
const mc = new MessageChannel();
|
|
2038
|
+
this.asyncWorker.postMessage(
|
|
2039
|
+
{ type: "init-port", port: mc.port1 },
|
|
2040
|
+
[mc.port1]
|
|
2041
|
+
);
|
|
2042
|
+
this.syncWorker.postMessage(
|
|
2043
|
+
{ type: "async-port", port: mc.port2 },
|
|
2044
|
+
[mc.port2]
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
if (newMode === "opfs") {
|
|
2048
|
+
this.sendOPFSInit();
|
|
2049
|
+
} else {
|
|
2050
|
+
this.sendLeaderInit();
|
|
2051
|
+
}
|
|
1891
2052
|
return this.readyPromise;
|
|
1892
2053
|
}
|
|
1893
2054
|
};
|
|
@@ -2066,6 +2227,7 @@ var VFSEngine = class {
|
|
|
2066
2227
|
this.bitmap = new Uint8Array(layout.bitmapSize);
|
|
2067
2228
|
this.handle.write(this.bitmap, { at: this.bitmapOffset });
|
|
2068
2229
|
this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
|
|
2230
|
+
this.writeSuperblock();
|
|
2069
2231
|
this.handle.flush();
|
|
2070
2232
|
}
|
|
2071
2233
|
/** Mount an existing VFS from disk — validates superblock integrity */
|
|
@@ -2225,14 +2387,33 @@ var VFSEngine = class {
|
|
|
2225
2387
|
const off = i * INODE_SIZE;
|
|
2226
2388
|
const type = inodeView.getUint8(off + INODE.TYPE);
|
|
2227
2389
|
if (type === INODE_TYPE.FREE) continue;
|
|
2390
|
+
if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) {
|
|
2391
|
+
throw new Error(`Corrupt VFS: inode ${i} has invalid type ${type}`);
|
|
2392
|
+
}
|
|
2393
|
+
const pathOffset = inodeView.getUint32(off + INODE.PATH_OFFSET, true);
|
|
2394
|
+
const pathLength = inodeView.getUint16(off + INODE.PATH_LENGTH, true);
|
|
2395
|
+
const size = inodeView.getFloat64(off + INODE.SIZE, true);
|
|
2396
|
+
const firstBlock = inodeView.getUint32(off + INODE.FIRST_BLOCK, true);
|
|
2397
|
+
const blockCount = inodeView.getUint32(off + INODE.BLOCK_COUNT, true);
|
|
2398
|
+
if (pathLength === 0 || pathOffset + pathLength > this.pathTableUsed) {
|
|
2399
|
+
throw new Error(`Corrupt VFS: inode ${i} path out of bounds (offset=${pathOffset}, len=${pathLength}, tableUsed=${this.pathTableUsed})`);
|
|
2400
|
+
}
|
|
2401
|
+
if (type !== INODE_TYPE.DIRECTORY) {
|
|
2402
|
+
if (size < 0 || !isFinite(size)) {
|
|
2403
|
+
throw new Error(`Corrupt VFS: inode ${i} has invalid size ${size}`);
|
|
2404
|
+
}
|
|
2405
|
+
if (blockCount > 0 && firstBlock + blockCount > this.totalBlocks) {
|
|
2406
|
+
throw new Error(`Corrupt VFS: inode ${i} data blocks out of range (first=${firstBlock}, count=${blockCount}, total=${this.totalBlocks})`);
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2228
2409
|
const inode = {
|
|
2229
2410
|
type,
|
|
2230
|
-
pathOffset
|
|
2231
|
-
pathLength
|
|
2411
|
+
pathOffset,
|
|
2412
|
+
pathLength,
|
|
2232
2413
|
mode: inodeView.getUint32(off + INODE.MODE, true),
|
|
2233
|
-
size
|
|
2234
|
-
firstBlock
|
|
2235
|
-
blockCount
|
|
2414
|
+
size,
|
|
2415
|
+
firstBlock,
|
|
2416
|
+
blockCount,
|
|
2236
2417
|
mtime: inodeView.getFloat64(off + INODE.MTIME, true),
|
|
2237
2418
|
ctime: inodeView.getFloat64(off + INODE.CTIME, true),
|
|
2238
2419
|
atime: inodeView.getFloat64(off + INODE.ATIME, true),
|
|
@@ -2240,7 +2421,15 @@ var VFSEngine = class {
|
|
|
2240
2421
|
gid: inodeView.getUint32(off + INODE.GID, true)
|
|
2241
2422
|
};
|
|
2242
2423
|
this.inodeCache.set(i, inode);
|
|
2243
|
-
|
|
2424
|
+
let path;
|
|
2425
|
+
if (pathBuf) {
|
|
2426
|
+
path = decoder8.decode(pathBuf.subarray(inode.pathOffset, inode.pathOffset + inode.pathLength));
|
|
2427
|
+
} else {
|
|
2428
|
+
path = this.readPath(inode.pathOffset, inode.pathLength);
|
|
2429
|
+
}
|
|
2430
|
+
if (!path.startsWith("/") || path.includes("\0")) {
|
|
2431
|
+
throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
|
|
2432
|
+
}
|
|
2244
2433
|
this.pathIndex.set(path, i);
|
|
2245
2434
|
}
|
|
2246
2435
|
}
|
|
@@ -3303,19 +3492,6 @@ async function openVFSHandle(fileHandle) {
|
|
|
3303
3492
|
return { handle: new MemoryHandle(data), isMemory: true };
|
|
3304
3493
|
}
|
|
3305
3494
|
}
|
|
3306
|
-
async function openFreshVFSHandle(fileHandle) {
|
|
3307
|
-
try {
|
|
3308
|
-
const handle = await fileHandle.createSyncAccessHandle();
|
|
3309
|
-
return { handle, isMemory: false };
|
|
3310
|
-
} catch {
|
|
3311
|
-
return { handle: new MemoryHandle(), isMemory: true };
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
async function saveMemoryHandle(fileHandle, memHandle) {
|
|
3315
|
-
const writable = await fileHandle.createWritable();
|
|
3316
|
-
await writable.write(memHandle.getBuffer());
|
|
3317
|
-
await writable.close();
|
|
3318
|
-
}
|
|
3319
3495
|
async function navigateToRoot(root) {
|
|
3320
3496
|
let dir = await navigator.storage.getDirectory();
|
|
3321
3497
|
if (root && root !== "/") {
|
|
@@ -3500,35 +3676,7 @@ async function loadFromOPFS(root = "/", fs) {
|
|
|
3500
3676
|
}
|
|
3501
3677
|
return { files, directories };
|
|
3502
3678
|
}
|
|
3503
|
-
|
|
3504
|
-
await rootDir.removeEntry(".vfs.bin");
|
|
3505
|
-
} catch (_) {
|
|
3506
|
-
}
|
|
3507
|
-
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
|
|
3508
|
-
const { handle, isMemory } = await openFreshVFSHandle(vfsFileHandle);
|
|
3509
|
-
try {
|
|
3510
|
-
const engine = new VFSEngine();
|
|
3511
|
-
engine.init(handle);
|
|
3512
|
-
const dirs = opfsEntries.filter((e) => e.type === "directory").sort((a, b) => a.path.localeCompare(b.path));
|
|
3513
|
-
let files = 0;
|
|
3514
|
-
let directories = 0;
|
|
3515
|
-
for (const dir of dirs) {
|
|
3516
|
-
engine.mkdir(dir.path, 16877);
|
|
3517
|
-
directories++;
|
|
3518
|
-
}
|
|
3519
|
-
const fileEntries = opfsEntries.filter((e) => e.type === "file");
|
|
3520
|
-
for (const file of fileEntries) {
|
|
3521
|
-
engine.write(file.path, new Uint8Array(file.data));
|
|
3522
|
-
files++;
|
|
3523
|
-
}
|
|
3524
|
-
engine.flush();
|
|
3525
|
-
if (isMemory) {
|
|
3526
|
-
await saveMemoryHandle(vfsFileHandle, handle);
|
|
3527
|
-
}
|
|
3528
|
-
return { files, directories };
|
|
3529
|
-
} finally {
|
|
3530
|
-
handle.close();
|
|
3531
|
-
}
|
|
3679
|
+
return spawnRepairWorker({ type: "load", root });
|
|
3532
3680
|
}
|
|
3533
3681
|
async function repairVFS(root = "/", fs) {
|
|
3534
3682
|
if (fs) {
|
|
@@ -3542,137 +3690,28 @@ async function repairVFS(root = "/", fs) {
|
|
|
3542
3690
|
// Detailed entries not available in fs-based path
|
|
3543
3691
|
};
|
|
3544
3692
|
}
|
|
3545
|
-
return
|
|
3693
|
+
return spawnRepairWorker({ type: "repair", root });
|
|
3546
3694
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
inodeCount = view.getUint32(SUPERBLOCK.INODE_COUNT, true);
|
|
3568
|
-
blockSize = view.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
|
|
3569
|
-
totalBlocks = view.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
|
|
3570
|
-
inodeTableOffset = view.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
|
|
3571
|
-
pathTableOffset = view.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
|
|
3572
|
-
view.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
|
|
3573
|
-
dataOffset = view.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
|
|
3574
|
-
view.getUint32(SUPERBLOCK.PATH_USED, true);
|
|
3575
|
-
if (blockSize === 0 || (blockSize & blockSize - 1) !== 0 || inodeCount === 0 || inodeTableOffset >= fileSize || pathTableOffset >= fileSize || dataOffset >= fileSize) {
|
|
3576
|
-
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
3577
|
-
inodeCount = DEFAULT_INODE_COUNT;
|
|
3578
|
-
blockSize = DEFAULT_BLOCK_SIZE;
|
|
3579
|
-
totalBlocks = INITIAL_DATA_BLOCKS;
|
|
3580
|
-
inodeTableOffset = layout.inodeTableOffset;
|
|
3581
|
-
pathTableOffset = layout.pathTableOffset;
|
|
3582
|
-
dataOffset = layout.dataOffset;
|
|
3583
|
-
}
|
|
3584
|
-
} else {
|
|
3585
|
-
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
3586
|
-
inodeCount = DEFAULT_INODE_COUNT;
|
|
3587
|
-
blockSize = DEFAULT_BLOCK_SIZE;
|
|
3588
|
-
totalBlocks = INITIAL_DATA_BLOCKS;
|
|
3589
|
-
inodeTableOffset = layout.inodeTableOffset;
|
|
3590
|
-
pathTableOffset = layout.pathTableOffset;
|
|
3591
|
-
dataOffset = layout.dataOffset;
|
|
3592
|
-
}
|
|
3593
|
-
const decoder9 = new TextDecoder();
|
|
3594
|
-
const recovered = [];
|
|
3595
|
-
let lost = 0;
|
|
3596
|
-
const maxInodes = Math.min(inodeCount, Math.floor((fileSize - inodeTableOffset) / INODE_SIZE));
|
|
3597
|
-
for (let i = 0; i < maxInodes; i++) {
|
|
3598
|
-
const off = inodeTableOffset + i * INODE_SIZE;
|
|
3599
|
-
if (off + INODE_SIZE > fileSize) break;
|
|
3600
|
-
const type = raw[off + INODE.TYPE];
|
|
3601
|
-
if (type < INODE_TYPE.FILE || type > INODE_TYPE.SYMLINK) continue;
|
|
3602
|
-
const inodeView = new DataView(raw.buffer, off, INODE_SIZE);
|
|
3603
|
-
const pathOffset = inodeView.getUint32(INODE.PATH_OFFSET, true);
|
|
3604
|
-
const pathLength = inodeView.getUint16(INODE.PATH_LENGTH, true);
|
|
3605
|
-
const size = inodeView.getFloat64(INODE.SIZE, true);
|
|
3606
|
-
const firstBlock = inodeView.getUint32(INODE.FIRST_BLOCK, true);
|
|
3607
|
-
inodeView.getUint32(INODE.BLOCK_COUNT, true);
|
|
3608
|
-
const absPathOffset = pathTableOffset + pathOffset;
|
|
3609
|
-
if (pathLength === 0 || pathLength > 4096 || absPathOffset + pathLength > fileSize) {
|
|
3610
|
-
lost++;
|
|
3611
|
-
continue;
|
|
3612
|
-
}
|
|
3613
|
-
let path;
|
|
3614
|
-
try {
|
|
3615
|
-
path = decoder9.decode(raw.subarray(absPathOffset, absPathOffset + pathLength));
|
|
3616
|
-
} catch {
|
|
3617
|
-
lost++;
|
|
3618
|
-
continue;
|
|
3619
|
-
}
|
|
3620
|
-
if (!path.startsWith("/") || path.includes("\0")) {
|
|
3621
|
-
lost++;
|
|
3622
|
-
continue;
|
|
3623
|
-
}
|
|
3624
|
-
if (type === INODE_TYPE.DIRECTORY) {
|
|
3625
|
-
recovered.push({ path, type, data: new Uint8Array(0) });
|
|
3626
|
-
continue;
|
|
3627
|
-
}
|
|
3628
|
-
if (size < 0 || size > fileSize || !isFinite(size)) {
|
|
3629
|
-
lost++;
|
|
3630
|
-
continue;
|
|
3631
|
-
}
|
|
3632
|
-
const dataStart = dataOffset + firstBlock * blockSize;
|
|
3633
|
-
if (dataStart + size > fileSize || firstBlock >= totalBlocks) {
|
|
3634
|
-
recovered.push({ path, type, data: new Uint8Array(0) });
|
|
3635
|
-
lost++;
|
|
3636
|
-
continue;
|
|
3637
|
-
}
|
|
3638
|
-
const data = raw.slice(dataStart, dataStart + size);
|
|
3639
|
-
recovered.push({ path, type, data });
|
|
3640
|
-
}
|
|
3641
|
-
await rootDir.removeEntry(".vfs.bin");
|
|
3642
|
-
const newFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
|
|
3643
|
-
const { handle, isMemory } = await openFreshVFSHandle(newFileHandle);
|
|
3644
|
-
try {
|
|
3645
|
-
const engine = new VFSEngine();
|
|
3646
|
-
engine.init(handle);
|
|
3647
|
-
const dirs = recovered.filter((e) => e.type === INODE_TYPE.DIRECTORY && e.path !== "/").sort((a, b) => a.path.localeCompare(b.path));
|
|
3648
|
-
const files = recovered.filter((e) => e.type === INODE_TYPE.FILE);
|
|
3649
|
-
const symlinks = recovered.filter((e) => e.type === INODE_TYPE.SYMLINK);
|
|
3650
|
-
for (const dir of dirs) {
|
|
3651
|
-
const result = engine.mkdir(dir.path, 16877);
|
|
3652
|
-
if (result.status !== 0) lost++;
|
|
3653
|
-
}
|
|
3654
|
-
for (const file2 of files) {
|
|
3655
|
-
const result = engine.write(file2.path, file2.data);
|
|
3656
|
-
if (result.status !== 0) lost++;
|
|
3657
|
-
}
|
|
3658
|
-
for (const sym of symlinks) {
|
|
3659
|
-
const target = decoder9.decode(sym.data);
|
|
3660
|
-
const result = engine.symlink(target, sym.path);
|
|
3661
|
-
if (result.status !== 0) lost++;
|
|
3662
|
-
}
|
|
3663
|
-
engine.flush();
|
|
3664
|
-
if (isMemory) {
|
|
3665
|
-
await saveMemoryHandle(newFileHandle, handle);
|
|
3666
|
-
}
|
|
3667
|
-
} finally {
|
|
3668
|
-
handle.close();
|
|
3669
|
-
}
|
|
3670
|
-
const entries = recovered.filter((e) => e.path !== "/").map((e) => ({
|
|
3671
|
-
path: e.path,
|
|
3672
|
-
type: e.type === INODE_TYPE.FILE ? "file" : e.type === INODE_TYPE.DIRECTORY ? "directory" : "symlink",
|
|
3673
|
-
size: e.data.byteLength
|
|
3674
|
-
}));
|
|
3675
|
-
return { recovered: entries.length, lost, entries };
|
|
3695
|
+
function spawnRepairWorker(msg) {
|
|
3696
|
+
return new Promise((resolve2, reject) => {
|
|
3697
|
+
const worker = new Worker(
|
|
3698
|
+
new URL("./workers/repair.worker.js", import.meta.url),
|
|
3699
|
+
{ type: "module" }
|
|
3700
|
+
);
|
|
3701
|
+
worker.onmessage = (event) => {
|
|
3702
|
+
worker.terminate();
|
|
3703
|
+
if (event.data.error) {
|
|
3704
|
+
reject(new Error(event.data.error));
|
|
3705
|
+
} else {
|
|
3706
|
+
resolve2(event.data);
|
|
3707
|
+
}
|
|
3708
|
+
};
|
|
3709
|
+
worker.onerror = (event) => {
|
|
3710
|
+
worker.terminate();
|
|
3711
|
+
reject(new Error(event.message || "Repair worker failed"));
|
|
3712
|
+
};
|
|
3713
|
+
worker.postMessage(msg);
|
|
3714
|
+
});
|
|
3676
3715
|
}
|
|
3677
3716
|
|
|
3678
3717
|
// src/index.ts
|