@componentor/fs 3.0.8 → 3.0.10
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 +109 -10
- package/dist/index.js +316 -185
- 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
|
}
|
|
@@ -3283,7 +3472,6 @@ var MemoryHandle = class {
|
|
|
3283
3472
|
}
|
|
3284
3473
|
close() {
|
|
3285
3474
|
}
|
|
3286
|
-
/** Get the current data as an ArrayBuffer (trimmed to actual size) */
|
|
3287
3475
|
getBuffer() {
|
|
3288
3476
|
return this.buf.buffer.slice(0, this.len);
|
|
3289
3477
|
}
|
|
@@ -3304,19 +3492,6 @@ async function openVFSHandle(fileHandle) {
|
|
|
3304
3492
|
return { handle: new MemoryHandle(data), isMemory: true };
|
|
3305
3493
|
}
|
|
3306
3494
|
}
|
|
3307
|
-
async function openFreshVFSHandle(fileHandle) {
|
|
3308
|
-
try {
|
|
3309
|
-
const handle = await fileHandle.createSyncAccessHandle();
|
|
3310
|
-
return { handle, isMemory: false };
|
|
3311
|
-
} catch {
|
|
3312
|
-
return { handle: new MemoryHandle(), isMemory: true };
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
async function saveMemoryHandle(fileHandle, memHandle) {
|
|
3316
|
-
const writable = await fileHandle.createWritable();
|
|
3317
|
-
await writable.write(memHandle.getBuffer());
|
|
3318
|
-
await writable.close();
|
|
3319
|
-
}
|
|
3320
3495
|
async function navigateToRoot(root) {
|
|
3321
3496
|
let dir = await navigator.storage.getDirectory();
|
|
3322
3497
|
if (root && root !== "/") {
|
|
@@ -3386,8 +3561,52 @@ async function readOPFSRecursive(dir, prefix, skip) {
|
|
|
3386
3561
|
}
|
|
3387
3562
|
return result;
|
|
3388
3563
|
}
|
|
3389
|
-
|
|
3564
|
+
function readVFSRecursive(fs, vfsPath) {
|
|
3565
|
+
const result = [];
|
|
3566
|
+
let entries;
|
|
3567
|
+
try {
|
|
3568
|
+
entries = fs.readdirSync(vfsPath, { withFileTypes: true });
|
|
3569
|
+
} catch {
|
|
3570
|
+
return result;
|
|
3571
|
+
}
|
|
3572
|
+
for (const entry of entries) {
|
|
3573
|
+
const fullPath = vfsPath === "/" ? `/${entry.name}` : `${vfsPath}/${entry.name}`;
|
|
3574
|
+
if (entry.isDirectory()) {
|
|
3575
|
+
result.push({ path: fullPath, type: "directory" });
|
|
3576
|
+
result.push(...readVFSRecursive(fs, fullPath));
|
|
3577
|
+
} else {
|
|
3578
|
+
try {
|
|
3579
|
+
const data = fs.readFileSync(fullPath);
|
|
3580
|
+
result.push({ path: fullPath, type: "file", data });
|
|
3581
|
+
} catch {
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
return result;
|
|
3586
|
+
}
|
|
3587
|
+
async function unpackToOPFS(root = "/", fs) {
|
|
3390
3588
|
const rootDir = await navigateToRoot(root);
|
|
3589
|
+
if (fs) {
|
|
3590
|
+
const vfsEntries = readVFSRecursive(fs, "/");
|
|
3591
|
+
let files2 = 0;
|
|
3592
|
+
let directories2 = 0;
|
|
3593
|
+
for (const entry of vfsEntries) {
|
|
3594
|
+
if (entry.type === "directory") {
|
|
3595
|
+
const name = basename2(entry.path);
|
|
3596
|
+
const parent = await ensureParentDirs(rootDir, entry.path);
|
|
3597
|
+
await parent.getDirectoryHandle(name, { create: true });
|
|
3598
|
+
directories2++;
|
|
3599
|
+
} else {
|
|
3600
|
+
try {
|
|
3601
|
+
await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
|
|
3602
|
+
files2++;
|
|
3603
|
+
} catch (err) {
|
|
3604
|
+
console.warn(`[VFS] Failed to write OPFS file ${entry.path}: ${err.message}`);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
return { files: files2, directories: directories2 };
|
|
3609
|
+
}
|
|
3391
3610
|
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin");
|
|
3392
3611
|
const { handle } = await openVFSHandle(vfsFileHandle);
|
|
3393
3612
|
let entries;
|
|
@@ -3404,183 +3623,95 @@ async function unpackToOPFS(root = "/") {
|
|
|
3404
3623
|
for (const entry of entries) {
|
|
3405
3624
|
if (entry.path === "/") continue;
|
|
3406
3625
|
if (entry.type === INODE_TYPE.DIRECTORY) {
|
|
3407
|
-
await ensureParentDirs(rootDir, entry.path + "/dummy");
|
|
3408
3626
|
const name = basename2(entry.path);
|
|
3409
3627
|
const parent = await ensureParentDirs(rootDir, entry.path);
|
|
3410
3628
|
await parent.getDirectoryHandle(name, { create: true });
|
|
3411
3629
|
directories++;
|
|
3412
|
-
} else if (entry.type === INODE_TYPE.FILE) {
|
|
3413
|
-
await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
|
|
3414
|
-
files++;
|
|
3415
|
-
} else if (entry.type === INODE_TYPE.SYMLINK) {
|
|
3630
|
+
} else if (entry.type === INODE_TYPE.FILE || entry.type === INODE_TYPE.SYMLINK) {
|
|
3416
3631
|
await writeOPFSFile(rootDir, entry.path, entry.data ?? new Uint8Array(0));
|
|
3417
3632
|
files++;
|
|
3418
3633
|
}
|
|
3419
3634
|
}
|
|
3420
3635
|
return { files, directories };
|
|
3421
3636
|
}
|
|
3422
|
-
async function loadFromOPFS(root = "/") {
|
|
3637
|
+
async function loadFromOPFS(root = "/", fs) {
|
|
3423
3638
|
const rootDir = await navigateToRoot(root);
|
|
3424
3639
|
const opfsEntries = await readOPFSRecursive(rootDir, "", /* @__PURE__ */ new Set([".vfs.bin"]));
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3640
|
+
if (fs) {
|
|
3641
|
+
try {
|
|
3642
|
+
const rootEntries = fs.readdirSync("/");
|
|
3643
|
+
for (const entry of rootEntries) {
|
|
3644
|
+
try {
|
|
3645
|
+
fs.rmSync(`/${entry}`, { recursive: true, force: true });
|
|
3646
|
+
} catch {
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
} catch {
|
|
3650
|
+
}
|
|
3434
3651
|
const dirs = opfsEntries.filter((e) => e.type === "directory").sort((a, b) => a.path.localeCompare(b.path));
|
|
3435
3652
|
let files = 0;
|
|
3436
3653
|
let directories = 0;
|
|
3437
3654
|
for (const dir of dirs) {
|
|
3438
|
-
|
|
3439
|
-
|
|
3655
|
+
try {
|
|
3656
|
+
fs.mkdirSync(dir.path, { recursive: true, mode: 493 });
|
|
3657
|
+
directories++;
|
|
3658
|
+
} catch {
|
|
3659
|
+
}
|
|
3440
3660
|
}
|
|
3441
3661
|
const fileEntries = opfsEntries.filter((e) => e.type === "file");
|
|
3442
3662
|
for (const file of fileEntries) {
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3663
|
+
try {
|
|
3664
|
+
const parentPath = file.path.substring(0, file.path.lastIndexOf("/")) || "/";
|
|
3665
|
+
if (parentPath !== "/") {
|
|
3666
|
+
try {
|
|
3667
|
+
fs.mkdirSync(parentPath, { recursive: true, mode: 493 });
|
|
3668
|
+
} catch {
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
fs.writeFileSync(file.path, new Uint8Array(file.data));
|
|
3672
|
+
files++;
|
|
3673
|
+
} catch (err) {
|
|
3674
|
+
console.warn(`[VFS] Failed to write ${file.path}: ${err.message}`);
|
|
3675
|
+
}
|
|
3449
3676
|
}
|
|
3450
3677
|
return { files, directories };
|
|
3451
|
-
} finally {
|
|
3452
|
-
handle.close();
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
async function repairVFS(root = "/") {
|
|
3456
|
-
const rootDir = await navigateToRoot(root);
|
|
3457
|
-
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin");
|
|
3458
|
-
const file = await vfsFileHandle.getFile();
|
|
3459
|
-
const raw = new Uint8Array(await file.arrayBuffer());
|
|
3460
|
-
const fileSize = raw.byteLength;
|
|
3461
|
-
if (fileSize < SUPERBLOCK.SIZE) {
|
|
3462
|
-
throw new Error(`VFS file too small to repair (${fileSize} bytes)`);
|
|
3463
|
-
}
|
|
3464
|
-
const view = new DataView(raw.buffer);
|
|
3465
|
-
let inodeCount;
|
|
3466
|
-
let blockSize;
|
|
3467
|
-
let totalBlocks;
|
|
3468
|
-
let inodeTableOffset;
|
|
3469
|
-
let pathTableOffset;
|
|
3470
|
-
let dataOffset;
|
|
3471
|
-
const magic = view.getUint32(SUPERBLOCK.MAGIC, true);
|
|
3472
|
-
const version = view.getUint32(SUPERBLOCK.VERSION, true);
|
|
3473
|
-
const superblockValid = magic === VFS_MAGIC && version === VFS_VERSION;
|
|
3474
|
-
if (superblockValid) {
|
|
3475
|
-
inodeCount = view.getUint32(SUPERBLOCK.INODE_COUNT, true);
|
|
3476
|
-
blockSize = view.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
|
|
3477
|
-
totalBlocks = view.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
|
|
3478
|
-
inodeTableOffset = view.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
|
|
3479
|
-
pathTableOffset = view.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
|
|
3480
|
-
view.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
|
|
3481
|
-
dataOffset = view.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
|
|
3482
|
-
view.getUint32(SUPERBLOCK.PATH_USED, true);
|
|
3483
|
-
if (blockSize === 0 || (blockSize & blockSize - 1) !== 0 || inodeCount === 0 || inodeTableOffset >= fileSize || pathTableOffset >= fileSize || dataOffset >= fileSize) {
|
|
3484
|
-
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
3485
|
-
inodeCount = DEFAULT_INODE_COUNT;
|
|
3486
|
-
blockSize = DEFAULT_BLOCK_SIZE;
|
|
3487
|
-
totalBlocks = INITIAL_DATA_BLOCKS;
|
|
3488
|
-
inodeTableOffset = layout.inodeTableOffset;
|
|
3489
|
-
pathTableOffset = layout.pathTableOffset;
|
|
3490
|
-
dataOffset = layout.dataOffset;
|
|
3491
|
-
}
|
|
3492
|
-
} else {
|
|
3493
|
-
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
3494
|
-
inodeCount = DEFAULT_INODE_COUNT;
|
|
3495
|
-
blockSize = DEFAULT_BLOCK_SIZE;
|
|
3496
|
-
totalBlocks = INITIAL_DATA_BLOCKS;
|
|
3497
|
-
inodeTableOffset = layout.inodeTableOffset;
|
|
3498
|
-
pathTableOffset = layout.pathTableOffset;
|
|
3499
|
-
dataOffset = layout.dataOffset;
|
|
3500
3678
|
}
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
const firstBlock = inodeView.getUint32(INODE.FIRST_BLOCK, true);
|
|
3515
|
-
inodeView.getUint32(INODE.BLOCK_COUNT, true);
|
|
3516
|
-
const absPathOffset = pathTableOffset + pathOffset;
|
|
3517
|
-
if (pathLength === 0 || pathLength > 4096 || absPathOffset + pathLength > fileSize) {
|
|
3518
|
-
lost++;
|
|
3519
|
-
continue;
|
|
3520
|
-
}
|
|
3521
|
-
let path;
|
|
3522
|
-
try {
|
|
3523
|
-
path = decoder9.decode(raw.subarray(absPathOffset, absPathOffset + pathLength));
|
|
3524
|
-
} catch {
|
|
3525
|
-
lost++;
|
|
3526
|
-
continue;
|
|
3527
|
-
}
|
|
3528
|
-
if (!path.startsWith("/") || path.includes("\0")) {
|
|
3529
|
-
lost++;
|
|
3530
|
-
continue;
|
|
3531
|
-
}
|
|
3532
|
-
if (type === INODE_TYPE.DIRECTORY) {
|
|
3533
|
-
recovered.push({ path, type, data: new Uint8Array(0) });
|
|
3534
|
-
continue;
|
|
3535
|
-
}
|
|
3536
|
-
if (size < 0 || size > fileSize || !isFinite(size)) {
|
|
3537
|
-
lost++;
|
|
3538
|
-
continue;
|
|
3539
|
-
}
|
|
3540
|
-
const dataStart = dataOffset + firstBlock * blockSize;
|
|
3541
|
-
if (dataStart + size > fileSize || firstBlock >= totalBlocks) {
|
|
3542
|
-
recovered.push({ path, type, data: new Uint8Array(0) });
|
|
3543
|
-
lost++;
|
|
3544
|
-
continue;
|
|
3545
|
-
}
|
|
3546
|
-
const data = raw.slice(dataStart, dataStart + size);
|
|
3547
|
-
recovered.push({ path, type, data });
|
|
3548
|
-
}
|
|
3549
|
-
await rootDir.removeEntry(".vfs.bin");
|
|
3550
|
-
const newFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
|
|
3551
|
-
const { handle, isMemory } = await openFreshVFSHandle(newFileHandle);
|
|
3552
|
-
try {
|
|
3553
|
-
const engine = new VFSEngine();
|
|
3554
|
-
engine.init(handle);
|
|
3555
|
-
const dirs = recovered.filter((e) => e.type === INODE_TYPE.DIRECTORY && e.path !== "/").sort((a, b) => a.path.localeCompare(b.path));
|
|
3556
|
-
const files = recovered.filter((e) => e.type === INODE_TYPE.FILE);
|
|
3557
|
-
const symlinks = recovered.filter((e) => e.type === INODE_TYPE.SYMLINK);
|
|
3558
|
-
for (const dir of dirs) {
|
|
3559
|
-
const result = engine.mkdir(dir.path, 16877);
|
|
3560
|
-
if (result.status !== 0) lost++;
|
|
3561
|
-
}
|
|
3562
|
-
for (const file2 of files) {
|
|
3563
|
-
const result = engine.write(file2.path, file2.data);
|
|
3564
|
-
if (result.status !== 0) lost++;
|
|
3565
|
-
}
|
|
3566
|
-
for (const sym of symlinks) {
|
|
3567
|
-
const target = decoder9.decode(sym.data);
|
|
3568
|
-
const result = engine.symlink(target, sym.path);
|
|
3569
|
-
if (result.status !== 0) lost++;
|
|
3570
|
-
}
|
|
3571
|
-
engine.flush();
|
|
3572
|
-
if (isMemory) {
|
|
3573
|
-
await saveMemoryHandle(newFileHandle, handle);
|
|
3574
|
-
}
|
|
3575
|
-
} finally {
|
|
3576
|
-
handle.close();
|
|
3679
|
+
return spawnRepairWorker({ type: "load", root });
|
|
3680
|
+
}
|
|
3681
|
+
async function repairVFS(root = "/", fs) {
|
|
3682
|
+
if (fs) {
|
|
3683
|
+
const loadResult = await loadFromOPFS(root, fs);
|
|
3684
|
+
await unpackToOPFS(root, fs);
|
|
3685
|
+
const total = loadResult.files + loadResult.directories;
|
|
3686
|
+
return {
|
|
3687
|
+
recovered: total,
|
|
3688
|
+
lost: 0,
|
|
3689
|
+
entries: []
|
|
3690
|
+
// Detailed entries not available in fs-based path
|
|
3691
|
+
};
|
|
3577
3692
|
}
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3693
|
+
return spawnRepairWorker({ type: "repair", root });
|
|
3694
|
+
}
|
|
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
|
+
});
|
|
3584
3715
|
}
|
|
3585
3716
|
|
|
3586
3717
|
// src/index.ts
|