@componentor/fs 3.0.2 → 3.0.4
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 +74 -16
- package/dist/index.js +1773 -29
- package/dist/index.js.map +1 -1
- package/dist/workers/server.worker.js +88 -13
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +167 -33
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -168,6 +168,13 @@ var VFSEngine = class {
|
|
|
168
168
|
this.mount();
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
+
/** Release the sync access handle (call on fatal error or shutdown) */
|
|
172
|
+
closeHandle() {
|
|
173
|
+
try {
|
|
174
|
+
this.handle?.close();
|
|
175
|
+
} catch (_) {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
171
178
|
/** Format a fresh VFS */
|
|
172
179
|
format() {
|
|
173
180
|
const layout = calculateLayout(DEFAULT_INODE_COUNT, DEFAULT_BLOCK_SIZE, INITIAL_DATA_BLOCKS);
|
|
@@ -190,28 +197,78 @@ var VFSEngine = class {
|
|
|
190
197
|
this.createInode("/", INODE_TYPE.DIRECTORY, DEFAULT_DIR_MODE, 0);
|
|
191
198
|
this.handle.flush();
|
|
192
199
|
}
|
|
193
|
-
/** Mount an existing VFS from disk */
|
|
200
|
+
/** Mount an existing VFS from disk — validates superblock integrity */
|
|
194
201
|
mount() {
|
|
202
|
+
const fileSize = this.handle.getSize();
|
|
203
|
+
if (fileSize < SUPERBLOCK.SIZE) {
|
|
204
|
+
throw new Error(`Corrupt VFS: file too small (${fileSize} bytes, need at least ${SUPERBLOCK.SIZE})`);
|
|
205
|
+
}
|
|
195
206
|
this.handle.read(this.superblockBuf, { at: 0 });
|
|
196
207
|
const v = this.superblockView;
|
|
197
208
|
const magic = v.getUint32(SUPERBLOCK.MAGIC, true);
|
|
198
209
|
if (magic !== VFS_MAGIC) {
|
|
199
|
-
throw new Error(`
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
throw new Error(`Corrupt VFS: bad magic 0x${magic.toString(16)} (expected 0x${VFS_MAGIC.toString(16)})`);
|
|
211
|
+
}
|
|
212
|
+
const version = v.getUint32(SUPERBLOCK.VERSION, true);
|
|
213
|
+
if (version !== VFS_VERSION) {
|
|
214
|
+
throw new Error(`Corrupt VFS: unsupported version ${version} (expected ${VFS_VERSION})`);
|
|
215
|
+
}
|
|
216
|
+
const inodeCount = v.getUint32(SUPERBLOCK.INODE_COUNT, true);
|
|
217
|
+
const blockSize = v.getUint32(SUPERBLOCK.BLOCK_SIZE, true);
|
|
218
|
+
const totalBlocks = v.getUint32(SUPERBLOCK.TOTAL_BLOCKS, true);
|
|
219
|
+
const freeBlocks = v.getUint32(SUPERBLOCK.FREE_BLOCKS, true);
|
|
220
|
+
const inodeTableOffset = v.getFloat64(SUPERBLOCK.INODE_OFFSET, true);
|
|
221
|
+
const pathTableOffset = v.getFloat64(SUPERBLOCK.PATH_OFFSET, true);
|
|
222
|
+
const dataOffset = v.getFloat64(SUPERBLOCK.DATA_OFFSET, true);
|
|
223
|
+
const bitmapOffset = v.getFloat64(SUPERBLOCK.BITMAP_OFFSET, true);
|
|
224
|
+
const pathUsed = v.getUint32(SUPERBLOCK.PATH_USED, true);
|
|
225
|
+
if (blockSize === 0 || (blockSize & blockSize - 1) !== 0) {
|
|
226
|
+
throw new Error(`Corrupt VFS: invalid block size ${blockSize} (must be power of 2)`);
|
|
227
|
+
}
|
|
228
|
+
if (inodeCount === 0) {
|
|
229
|
+
throw new Error("Corrupt VFS: inode count is 0");
|
|
230
|
+
}
|
|
231
|
+
if (freeBlocks > totalBlocks) {
|
|
232
|
+
throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
|
|
233
|
+
}
|
|
234
|
+
if (inodeTableOffset !== SUPERBLOCK.SIZE) {
|
|
235
|
+
throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
|
|
236
|
+
}
|
|
237
|
+
const expectedPathOffset = inodeTableOffset + inodeCount * INODE_SIZE;
|
|
238
|
+
if (pathTableOffset !== expectedPathOffset) {
|
|
239
|
+
throw new Error(`Corrupt VFS: path table offset ${pathTableOffset} (expected ${expectedPathOffset})`);
|
|
240
|
+
}
|
|
241
|
+
if (bitmapOffset <= pathTableOffset) {
|
|
242
|
+
throw new Error(`Corrupt VFS: bitmap offset ${bitmapOffset} must be after path table ${pathTableOffset}`);
|
|
243
|
+
}
|
|
244
|
+
if (dataOffset <= bitmapOffset) {
|
|
245
|
+
throw new Error(`Corrupt VFS: data offset ${dataOffset} must be after bitmap ${bitmapOffset}`);
|
|
246
|
+
}
|
|
247
|
+
const pathTableSize = bitmapOffset - pathTableOffset;
|
|
248
|
+
if (pathUsed > pathTableSize) {
|
|
249
|
+
throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
|
|
250
|
+
}
|
|
251
|
+
const expectedMinSize = dataOffset + totalBlocks * blockSize;
|
|
252
|
+
if (fileSize < expectedMinSize) {
|
|
253
|
+
throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
|
|
254
|
+
}
|
|
255
|
+
this.inodeCount = inodeCount;
|
|
256
|
+
this.blockSize = blockSize;
|
|
257
|
+
this.totalBlocks = totalBlocks;
|
|
258
|
+
this.freeBlocks = freeBlocks;
|
|
259
|
+
this.inodeTableOffset = inodeTableOffset;
|
|
260
|
+
this.pathTableOffset = pathTableOffset;
|
|
261
|
+
this.dataOffset = dataOffset;
|
|
262
|
+
this.bitmapOffset = bitmapOffset;
|
|
263
|
+
this.pathTableUsed = pathUsed;
|
|
264
|
+
this.pathTableSize = pathTableSize;
|
|
211
265
|
const bitmapSize = Math.ceil(this.totalBlocks / 8);
|
|
212
266
|
this.bitmap = new Uint8Array(bitmapSize);
|
|
213
267
|
this.handle.read(this.bitmap, { at: this.bitmapOffset });
|
|
214
268
|
this.rebuildIndex();
|
|
269
|
+
if (!this.pathIndex.has("/")) {
|
|
270
|
+
throw new Error('Corrupt VFS: root directory "/" not found in inode table');
|
|
271
|
+
}
|
|
215
272
|
}
|
|
216
273
|
writeSuperblock() {
|
|
217
274
|
const v = this.superblockView;
|
|
@@ -1222,6 +1279,24 @@ var VFSEngine = class {
|
|
|
1222
1279
|
const data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
1223
1280
|
return { type: inode.type, data, mtime: inode.mtime };
|
|
1224
1281
|
}
|
|
1282
|
+
/** Export all files/dirs/symlinks from the VFS */
|
|
1283
|
+
exportAll() {
|
|
1284
|
+
const result = [];
|
|
1285
|
+
for (const [path, idx] of this.pathIndex) {
|
|
1286
|
+
const inode = this.readInode(idx);
|
|
1287
|
+
let data = null;
|
|
1288
|
+
if (inode.type === INODE_TYPE.FILE || inode.type === INODE_TYPE.SYMLINK) {
|
|
1289
|
+
data = inode.size > 0 ? this.readData(inode.firstBlock, inode.blockCount, inode.size) : new Uint8Array(0);
|
|
1290
|
+
}
|
|
1291
|
+
result.push({ path, type: inode.type, data, mode: inode.mode, mtime: inode.mtime });
|
|
1292
|
+
}
|
|
1293
|
+
result.sort((a, b) => {
|
|
1294
|
+
if (a.type === INODE_TYPE.DIRECTORY && b.type !== INODE_TYPE.DIRECTORY) return -1;
|
|
1295
|
+
if (a.type !== INODE_TYPE.DIRECTORY && b.type === INODE_TYPE.DIRECTORY) return 1;
|
|
1296
|
+
return a.path.localeCompare(b.path);
|
|
1297
|
+
});
|
|
1298
|
+
return result;
|
|
1299
|
+
}
|
|
1225
1300
|
flush() {
|
|
1226
1301
|
this.handle.flush();
|
|
1227
1302
|
}
|
|
@@ -1324,6 +1399,7 @@ var leaderLoopRunning = false;
|
|
|
1324
1399
|
var opfsSyncPort = null;
|
|
1325
1400
|
var opfsSyncEnabled = false;
|
|
1326
1401
|
var suppressPaths = /* @__PURE__ */ new Set();
|
|
1402
|
+
var watchBc = null;
|
|
1327
1403
|
var sab;
|
|
1328
1404
|
var ctrl;
|
|
1329
1405
|
var readySab;
|
|
@@ -1450,21 +1526,21 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1450
1526
|
break;
|
|
1451
1527
|
case OP.WRITE:
|
|
1452
1528
|
result = engine.write(path, data ?? new Uint8Array(0), flags);
|
|
1453
|
-
if (
|
|
1529
|
+
if (result.status === 0) {
|
|
1454
1530
|
syncOp = op;
|
|
1455
1531
|
syncPath = path;
|
|
1456
1532
|
}
|
|
1457
1533
|
break;
|
|
1458
1534
|
case OP.APPEND:
|
|
1459
1535
|
result = engine.append(path, data ?? new Uint8Array(0));
|
|
1460
|
-
if (
|
|
1536
|
+
if (result.status === 0) {
|
|
1461
1537
|
syncOp = op;
|
|
1462
1538
|
syncPath = path;
|
|
1463
1539
|
}
|
|
1464
1540
|
break;
|
|
1465
1541
|
case OP.UNLINK:
|
|
1466
1542
|
result = engine.unlink(path);
|
|
1467
|
-
if (
|
|
1543
|
+
if (result.status === 0) {
|
|
1468
1544
|
syncOp = op;
|
|
1469
1545
|
syncPath = path;
|
|
1470
1546
|
}
|
|
@@ -1477,14 +1553,14 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1477
1553
|
break;
|
|
1478
1554
|
case OP.MKDIR:
|
|
1479
1555
|
result = engine.mkdir(path, flags);
|
|
1480
|
-
if (
|
|
1556
|
+
if (result.status === 0) {
|
|
1481
1557
|
syncOp = op;
|
|
1482
1558
|
syncPath = path;
|
|
1483
1559
|
}
|
|
1484
1560
|
break;
|
|
1485
1561
|
case OP.RMDIR:
|
|
1486
1562
|
result = engine.rmdir(path, flags);
|
|
1487
|
-
if (
|
|
1563
|
+
if (result.status === 0) {
|
|
1488
1564
|
syncOp = op;
|
|
1489
1565
|
syncPath = path;
|
|
1490
1566
|
}
|
|
@@ -1495,7 +1571,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1495
1571
|
case OP.RENAME: {
|
|
1496
1572
|
const newPath = data ? decodeSecondPath(data) : "";
|
|
1497
1573
|
result = engine.rename(path, newPath);
|
|
1498
|
-
if (
|
|
1574
|
+
if (result.status === 0) {
|
|
1499
1575
|
syncOp = op;
|
|
1500
1576
|
syncPath = path;
|
|
1501
1577
|
syncNewPath = newPath;
|
|
@@ -1508,7 +1584,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1508
1584
|
case OP.TRUNCATE: {
|
|
1509
1585
|
const len = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
|
|
1510
1586
|
result = engine.truncate(path, len);
|
|
1511
|
-
if (
|
|
1587
|
+
if (result.status === 0) {
|
|
1512
1588
|
syncOp = op;
|
|
1513
1589
|
syncPath = path;
|
|
1514
1590
|
}
|
|
@@ -1517,7 +1593,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1517
1593
|
case OP.COPY: {
|
|
1518
1594
|
const destPath = data ? decodeSecondPath(data) : "";
|
|
1519
1595
|
result = engine.copy(path, destPath, flags);
|
|
1520
|
-
if (
|
|
1596
|
+
if (result.status === 0) {
|
|
1521
1597
|
syncOp = op;
|
|
1522
1598
|
syncPath = destPath;
|
|
1523
1599
|
}
|
|
@@ -1532,6 +1608,10 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1532
1608
|
case OP.CHMOD: {
|
|
1533
1609
|
const chmodMode = data ? new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(0, true) : 0;
|
|
1534
1610
|
result = engine.chmod(path, chmodMode);
|
|
1611
|
+
if (result.status === 0) {
|
|
1612
|
+
syncOp = op;
|
|
1613
|
+
syncPath = path;
|
|
1614
|
+
}
|
|
1535
1615
|
break;
|
|
1536
1616
|
}
|
|
1537
1617
|
case OP.CHOWN: {
|
|
@@ -1543,6 +1623,10 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1543
1623
|
const uid = dv.getUint32(0, true);
|
|
1544
1624
|
const gid = dv.getUint32(4, true);
|
|
1545
1625
|
result = engine.chown(path, uid, gid);
|
|
1626
|
+
if (result.status === 0) {
|
|
1627
|
+
syncOp = op;
|
|
1628
|
+
syncPath = path;
|
|
1629
|
+
}
|
|
1546
1630
|
break;
|
|
1547
1631
|
}
|
|
1548
1632
|
case OP.UTIMES: {
|
|
@@ -1554,12 +1638,16 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1554
1638
|
const atime = dv.getFloat64(0, true);
|
|
1555
1639
|
const mtime = dv.getFloat64(8, true);
|
|
1556
1640
|
result = engine.utimes(path, atime, mtime);
|
|
1641
|
+
if (result.status === 0) {
|
|
1642
|
+
syncOp = op;
|
|
1643
|
+
syncPath = path;
|
|
1644
|
+
}
|
|
1557
1645
|
break;
|
|
1558
1646
|
}
|
|
1559
1647
|
case OP.SYMLINK: {
|
|
1560
1648
|
const target = data ? new TextDecoder().decode(data) : "";
|
|
1561
1649
|
result = engine.symlink(target, path);
|
|
1562
|
-
if (
|
|
1650
|
+
if (result.status === 0) {
|
|
1563
1651
|
syncOp = op;
|
|
1564
1652
|
syncPath = path;
|
|
1565
1653
|
}
|
|
@@ -1571,7 +1659,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1571
1659
|
case OP.LINK: {
|
|
1572
1660
|
const newPath = data ? decodeSecondPath(data) : "";
|
|
1573
1661
|
result = engine.link(path, newPath);
|
|
1574
|
-
if (
|
|
1662
|
+
if (result.status === 0) {
|
|
1575
1663
|
syncOp = op;
|
|
1576
1664
|
syncPath = newPath;
|
|
1577
1665
|
}
|
|
@@ -1607,7 +1695,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1607
1695
|
const pos = dv.getInt32(4, true);
|
|
1608
1696
|
const writeData = data.subarray(8);
|
|
1609
1697
|
result = engine.fwrite(fd, writeData, pos === -1 ? null : pos);
|
|
1610
|
-
if (
|
|
1698
|
+
if (result.status === 0) {
|
|
1611
1699
|
syncOp = op;
|
|
1612
1700
|
syncPath = engine.getPathForFd(fd) ?? void 0;
|
|
1613
1701
|
}
|
|
@@ -1627,7 +1715,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1627
1715
|
const fd = dv.getUint32(0, true);
|
|
1628
1716
|
const len = dv.getUint32(4, true);
|
|
1629
1717
|
result = engine.ftruncate(fd, len);
|
|
1630
|
-
if (
|
|
1718
|
+
if (result.status === 0) {
|
|
1631
1719
|
syncOp = op;
|
|
1632
1720
|
syncPath = engine.getPathForFd(fd) ?? void 0;
|
|
1633
1721
|
}
|
|
@@ -1641,7 +1729,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1641
1729
|
break;
|
|
1642
1730
|
case OP.MKDTEMP:
|
|
1643
1731
|
result = engine.mkdtemp(path);
|
|
1644
|
-
if (
|
|
1732
|
+
if (result.status === 0 && result.data) {
|
|
1645
1733
|
syncOp = op;
|
|
1646
1734
|
syncPath = new TextDecoder().decode(result.data instanceof Uint8Array ? result.data : new Uint8Array(0));
|
|
1647
1735
|
}
|
|
@@ -1661,6 +1749,7 @@ function handleRequest(reqTabId, buffer) {
|
|
|
1661
1749
|
ret._op = syncOp;
|
|
1662
1750
|
ret._path = syncPath;
|
|
1663
1751
|
ret._newPath = syncNewPath;
|
|
1752
|
+
broadcastWatch(syncOp, syncPath, syncNewPath);
|
|
1664
1753
|
}
|
|
1665
1754
|
return ret;
|
|
1666
1755
|
}
|
|
@@ -1831,13 +1920,21 @@ async function initEngine(config) {
|
|
|
1831
1920
|
}
|
|
1832
1921
|
const vfsFileHandle = await rootDir.getFileHandle(".vfs.bin", { create: true });
|
|
1833
1922
|
const vfsHandle = await vfsFileHandle.createSyncAccessHandle();
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1923
|
+
try {
|
|
1924
|
+
engine.init(vfsHandle, {
|
|
1925
|
+
uid: config.uid,
|
|
1926
|
+
gid: config.gid,
|
|
1927
|
+
umask: config.umask,
|
|
1928
|
+
strictPermissions: config.strictPermissions,
|
|
1929
|
+
debug: config.debug
|
|
1930
|
+
});
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
try {
|
|
1933
|
+
vfsHandle.close();
|
|
1934
|
+
} catch (_) {
|
|
1935
|
+
}
|
|
1936
|
+
throw err;
|
|
1937
|
+
}
|
|
1841
1938
|
if (config.opfsSync) {
|
|
1842
1939
|
opfsSyncEnabled = true;
|
|
1843
1940
|
const mc = new MessageChannel();
|
|
@@ -1851,6 +1948,39 @@ async function initEngine(config) {
|
|
|
1851
1948
|
[mc.port2]
|
|
1852
1949
|
);
|
|
1853
1950
|
}
|
|
1951
|
+
watchBc = new BroadcastChannel("vfs-watch");
|
|
1952
|
+
}
|
|
1953
|
+
function broadcastWatch(op, path, newPath) {
|
|
1954
|
+
if (!watchBc) return;
|
|
1955
|
+
let eventType;
|
|
1956
|
+
switch (op) {
|
|
1957
|
+
case OP.WRITE:
|
|
1958
|
+
case OP.APPEND:
|
|
1959
|
+
case OP.TRUNCATE:
|
|
1960
|
+
case OP.FWRITE:
|
|
1961
|
+
case OP.FTRUNCATE:
|
|
1962
|
+
case OP.CHMOD:
|
|
1963
|
+
case OP.CHOWN:
|
|
1964
|
+
case OP.UTIMES:
|
|
1965
|
+
eventType = "change";
|
|
1966
|
+
break;
|
|
1967
|
+
case OP.UNLINK:
|
|
1968
|
+
case OP.RMDIR:
|
|
1969
|
+
case OP.RENAME:
|
|
1970
|
+
case OP.MKDIR:
|
|
1971
|
+
case OP.MKDTEMP:
|
|
1972
|
+
case OP.SYMLINK:
|
|
1973
|
+
case OP.LINK:
|
|
1974
|
+
case OP.COPY:
|
|
1975
|
+
eventType = "rename";
|
|
1976
|
+
break;
|
|
1977
|
+
default:
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
watchBc.postMessage({ eventType, path });
|
|
1981
|
+
if (op === OP.RENAME && newPath) {
|
|
1982
|
+
watchBc.postMessage({ eventType: "rename", path: newPath });
|
|
1983
|
+
}
|
|
1854
1984
|
}
|
|
1855
1985
|
function notifyOPFSSync(op, path, newPath) {
|
|
1856
1986
|
if (!opfsSyncPort) return;
|
|
@@ -1910,6 +2040,7 @@ function handleExternalChange(msg) {
|
|
|
1910
2040
|
case "external-write": {
|
|
1911
2041
|
suppressPaths.add(msg.path);
|
|
1912
2042
|
const result = engine.write(msg.path, new Uint8Array(msg.data), 0);
|
|
2043
|
+
if (result.status === 0) broadcastWatch(OP.WRITE, msg.path);
|
|
1913
2044
|
console.log("[sync-relay] external-write:", msg.path, `${msg.data?.byteLength ?? 0}B`, `status=${result.status}`);
|
|
1914
2045
|
break;
|
|
1915
2046
|
}
|
|
@@ -1918,8 +2049,10 @@ function handleExternalChange(msg) {
|
|
|
1918
2049
|
const result = engine.unlink(msg.path);
|
|
1919
2050
|
if (result.status !== 0) {
|
|
1920
2051
|
const rmdirResult = engine.rmdir(msg.path, 1);
|
|
2052
|
+
if (rmdirResult.status === 0) broadcastWatch(OP.RMDIR, msg.path);
|
|
1921
2053
|
console.log("[sync-relay] external-delete (rmdir):", msg.path, `status=${rmdirResult.status}`);
|
|
1922
2054
|
} else {
|
|
2055
|
+
broadcastWatch(OP.UNLINK, msg.path);
|
|
1923
2056
|
console.log("[sync-relay] external-delete:", msg.path, `status=${result.status}`);
|
|
1924
2057
|
}
|
|
1925
2058
|
break;
|
|
@@ -1929,6 +2062,7 @@ function handleExternalChange(msg) {
|
|
|
1929
2062
|
if (msg.newPath) {
|
|
1930
2063
|
suppressPaths.add(msg.newPath);
|
|
1931
2064
|
const result = engine.rename(msg.path, msg.newPath);
|
|
2065
|
+
if (result.status === 0) broadcastWatch(OP.RENAME, msg.path, msg.newPath);
|
|
1932
2066
|
console.log("[sync-relay] external-rename:", msg.path, "\u2192", msg.newPath, `status=${result.status}`);
|
|
1933
2067
|
}
|
|
1934
2068
|
break;
|