678 lines
20 KiB
TypeScript
678 lines
20 KiB
TypeScript
/**
|
|
* OPFS-backed filesystem for the FS Worker.
|
|
*
|
|
* Maintains a lightweight in-memory directory index (metadata only) for
|
|
* synchronous path resolution and readdir. File I/O uses OPFS
|
|
* SyncAccessHandles for synchronous read/write without any memfs layer.
|
|
*
|
|
* Async operations (open, mkdir, unlink, rmdir, rename) need OPFS directory
|
|
* mutations which are inherently async. The FS Worker handles these by
|
|
* briefly breaking out of its synchronous Atomics loop.
|
|
*/
|
|
|
|
// --- Directory index node types ---
|
|
|
|
interface FileNode {
|
|
name: string;
|
|
kind: 'file';
|
|
handle: FileSystemFileHandle;
|
|
size: number;
|
|
mtime: number;
|
|
}
|
|
|
|
interface DirNode {
|
|
name: string;
|
|
kind: 'directory';
|
|
handle: FileSystemDirectoryHandle;
|
|
children: Map<string, FileNode | DirNode>;
|
|
mtime: number;
|
|
}
|
|
|
|
type FsNode = FileNode | DirNode;
|
|
|
|
// --- Open file tracking ---
|
|
|
|
interface OpenFlags {
|
|
read: boolean;
|
|
write: boolean;
|
|
append: boolean;
|
|
create: boolean;
|
|
truncate: boolean;
|
|
exclusive: boolean;
|
|
}
|
|
|
|
/** Shared SyncAccessHandle with refcounting for multiple fds on the same file. */
|
|
interface SharedHandle {
|
|
syncHandle: FileSystemSyncAccessHandle;
|
|
node: FileNode;
|
|
refCount: number;
|
|
}
|
|
|
|
interface OpenFd {
|
|
/** Shared handle (null for directory fds). */
|
|
shared: SharedHandle | null;
|
|
/** The filesystem node (file or directory). */
|
|
node: FsNode;
|
|
/** Current read/write position. */
|
|
position: number;
|
|
flags: OpenFlags;
|
|
}
|
|
|
|
// --- Flag parsing ---
|
|
|
|
function parseOpenFlags(flags: number | string): OpenFlags {
|
|
if (typeof flags === 'string') {
|
|
const f = flags;
|
|
const plus = f.includes('+');
|
|
return {
|
|
read: f[0] === 'r' || plus,
|
|
write: f[0] === 'w' || f[0] === 'a' || plus,
|
|
append: f[0] === 'a',
|
|
create: f[0] === 'w' || f[0] === 'a',
|
|
truncate: f[0] === 'w' && !plus,
|
|
exclusive: f.includes('x'),
|
|
};
|
|
}
|
|
// Numeric flags (Node.js / Linux O_* values)
|
|
const access = flags & 3;
|
|
return {
|
|
read: access === 0 || access === 2, // O_RDONLY=0, O_RDWR=2
|
|
write: access === 1 || access === 2, // O_WRONLY=1, O_RDWR=2
|
|
append: !!(flags & 0o2000), // O_APPEND
|
|
create: !!(flags & 0o100), // O_CREAT
|
|
truncate: !!(flags & 0o1000), // O_TRUNC
|
|
exclusive: !!(flags & 0o200), // O_EXCL
|
|
};
|
|
}
|
|
|
|
// --- Stat result (compatible with serializeStat in fs-proxy-protocol) ---
|
|
|
|
class StatResult {
|
|
dev = 0;
|
|
ino: number;
|
|
mode: number;
|
|
nlink = 1;
|
|
uid = 0;
|
|
gid = 0;
|
|
rdev = 0;
|
|
size: number;
|
|
blksize = 4096;
|
|
blocks: number;
|
|
atimeMs: number;
|
|
mtimeMs: number;
|
|
ctimeMs: number;
|
|
birthtimeMs: number;
|
|
private _isFile: boolean;
|
|
|
|
constructor(isFile: boolean, size: number, mtime: number, ino: number) {
|
|
this._isFile = isFile;
|
|
this.ino = ino;
|
|
this.mode = isFile ? 0o100644 : 0o040755;
|
|
this.size = size;
|
|
this.blocks = Math.ceil(size / 512);
|
|
this.atimeMs = mtime;
|
|
this.mtimeMs = mtime;
|
|
this.ctimeMs = mtime;
|
|
this.birthtimeMs = mtime;
|
|
}
|
|
|
|
isFile() { return this._isFile; }
|
|
isDirectory() { return !this._isFile; }
|
|
isSymbolicLink() { return false; }
|
|
isCharacterDevice() { return false; }
|
|
isBlockDevice() { return false; }
|
|
isSocket() { return false; }
|
|
isFIFO() { return false; }
|
|
}
|
|
|
|
// --- Backend ---
|
|
|
|
export class OpfsBackend {
|
|
private root: DirNode | null = null;
|
|
private fdTable = new Map<number, OpenFd>();
|
|
/** Path → shared SyncAccessHandle for refcounting across multiple fds. */
|
|
private openHandles = new Map<string, SharedHandle>();
|
|
private nextFd = 3; // 0-2 reserved for stdin/stdout/stderr
|
|
private nextIno = 1;
|
|
|
|
/**
|
|
* Initialize: get the OPFS directory for this radio and build the
|
|
* in-memory directory index by scanning the OPFS tree.
|
|
*/
|
|
async init(radioKey: string): Promise<void> {
|
|
const storageRoot = await navigator.storage.getDirectory();
|
|
const edgetx = await storageRoot.getDirectoryHandle('edgetx-web', { create: true });
|
|
const radioDir = await edgetx.getDirectoryHandle(radioKey, { create: true });
|
|
|
|
this.root = {
|
|
name: '',
|
|
kind: 'directory',
|
|
handle: radioDir,
|
|
children: new Map(),
|
|
mtime: Date.now(),
|
|
};
|
|
|
|
await this.scanDir(this.root);
|
|
}
|
|
|
|
/** Whether the OPFS tree has any content for this radio. */
|
|
get hasContent(): boolean {
|
|
return this.root !== null && this.root.children.size > 0;
|
|
}
|
|
|
|
/** Close all open handles (for shutdown). */
|
|
closeAll(): void {
|
|
for (const [fd] of this.fdTable) {
|
|
try { this.close(fd); } catch { /* best effort */ }
|
|
}
|
|
}
|
|
|
|
// --- Async scan ---
|
|
|
|
private async scanDir(dir: DirNode): Promise<void> {
|
|
for await (const [name, handle] of (dir.handle as any).entries()) {
|
|
if (handle.kind === 'directory') {
|
|
const child: DirNode = {
|
|
name,
|
|
kind: 'directory',
|
|
handle: handle as FileSystemDirectoryHandle,
|
|
children: new Map(),
|
|
mtime: Date.now(),
|
|
};
|
|
dir.children.set(name, child);
|
|
await this.scanDir(child);
|
|
} else {
|
|
const file = await (handle as FileSystemFileHandle).getFile();
|
|
const child: FileNode = {
|
|
name,
|
|
kind: 'file',
|
|
handle: handle as FileSystemFileHandle,
|
|
size: file.size,
|
|
mtime: file.lastModified,
|
|
};
|
|
dir.children.set(name, child);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Synchronous path resolution ---
|
|
|
|
private normalizePath(path: string): string {
|
|
return '/' + path.split('/').filter(Boolean).join('/');
|
|
}
|
|
|
|
private resolve(path: string): FsNode {
|
|
if (!this.root) throw this.err('ENOENT', path);
|
|
|
|
const segments = path.split('/').filter(Boolean);
|
|
let node: FsNode = this.root;
|
|
|
|
for (const seg of segments) {
|
|
if (seg === '.') continue;
|
|
if (seg === '..') throw this.err('EINVAL', path);
|
|
if (node.kind !== 'directory') throw this.err('ENOTDIR', path);
|
|
const child = node.children.get(seg);
|
|
if (!child) throw this.err('ENOENT', path);
|
|
node = child;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
private resolveParent(path: string): { parent: DirNode; name: string } {
|
|
if (!this.root) throw this.err('ENOENT', path);
|
|
|
|
const segments = path.split('/').filter(Boolean);
|
|
if (segments.length === 0) throw this.err('EINVAL', path);
|
|
|
|
const name = segments.pop()!;
|
|
let node: FsNode = this.root;
|
|
|
|
for (const seg of segments) {
|
|
if (seg === '.') continue;
|
|
if (seg === '..') throw this.err('EINVAL', path);
|
|
if (node.kind !== 'directory') throw this.err('ENOTDIR', path);
|
|
const child = node.children.get(seg);
|
|
if (!child) throw this.err('ENOENT', path);
|
|
node = child;
|
|
}
|
|
|
|
if (node.kind !== 'directory') throw this.err('ENOTDIR', path);
|
|
return { parent: node, name };
|
|
}
|
|
|
|
// --- File operations ---
|
|
|
|
/** Open a file or directory. ASYNC (needs createSyncAccessHandle). */
|
|
async open(path: string, flags: number | string, _mode: number): Promise<number> {
|
|
const parsed = parseOpenFlags(flags);
|
|
const normPath = this.normalizePath(path);
|
|
|
|
let node: FsNode;
|
|
let created = false;
|
|
|
|
try {
|
|
node = this.resolve(path);
|
|
} catch (e: any) {
|
|
if (e.code === 'ENOENT' && parsed.create) {
|
|
node = await this.createFile(path);
|
|
created = true;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
if (!created && parsed.create && parsed.exclusive) {
|
|
throw this.err('EEXIST', path);
|
|
}
|
|
|
|
// Directory fd: no SyncAccessHandle, just a trackable fd for WASI
|
|
if (node.kind === 'directory') {
|
|
const fd = this.nextFd++;
|
|
this.fdTable.set(fd, { shared: null, node, position: 0, flags: parsed });
|
|
return fd;
|
|
}
|
|
|
|
// File fd: reuse existing SyncAccessHandle if already open (refcounting)
|
|
let shared = this.openHandles.get(normPath);
|
|
if (shared) {
|
|
shared.refCount++;
|
|
} else {
|
|
const syncHandle = await (node as FileNode).handle.createSyncAccessHandle();
|
|
shared = { syncHandle, node: node as FileNode, refCount: 1 };
|
|
this.openHandles.set(normPath, shared);
|
|
}
|
|
|
|
if (parsed.truncate) {
|
|
shared.syncHandle.truncate(0);
|
|
shared.node.size = 0;
|
|
shared.node.mtime = Date.now();
|
|
}
|
|
|
|
const fd = this.nextFd++;
|
|
this.fdTable.set(fd, {
|
|
shared,
|
|
node,
|
|
position: parsed.append ? shared.syncHandle.getSize() : 0,
|
|
flags: parsed,
|
|
});
|
|
|
|
return fd;
|
|
}
|
|
|
|
/** Close a file descriptor. Sync. */
|
|
close(fd: number): void {
|
|
const openFd = this.fdTable.get(fd);
|
|
if (!openFd) throw this.err('EBADF', `fd ${fd}`);
|
|
|
|
if (openFd.shared) {
|
|
openFd.shared.refCount--;
|
|
if (openFd.shared.refCount <= 0) {
|
|
openFd.shared.syncHandle.flush();
|
|
openFd.shared.node.size = openFd.shared.syncHandle.getSize();
|
|
openFd.shared.syncHandle.close();
|
|
// Remove from openHandles
|
|
for (const [path, h] of this.openHandles) {
|
|
if (h === openFd.shared) {
|
|
this.openHandles.delete(path);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.fdTable.delete(fd);
|
|
}
|
|
|
|
/** Read from an open file. Sync. */
|
|
read(fd: number, length: number, position: number | null): { data: Uint8Array; bytesRead: number } {
|
|
const openFd = this.getFd(fd, 'file');
|
|
const pos = position ?? openFd.position;
|
|
const buf = new Uint8Array(length);
|
|
const bytesRead = openFd.shared!.syncHandle.read(buf, { at: pos });
|
|
|
|
if (position === null) {
|
|
openFd.position += bytesRead;
|
|
}
|
|
|
|
return { data: buf, bytesRead };
|
|
}
|
|
|
|
/** Write to an open file. Sync. */
|
|
write(fd: number, data: Uint8Array, position: number | null): number {
|
|
const openFd = this.getFd(fd, 'file');
|
|
const sh = openFd.shared!;
|
|
|
|
let pos: number;
|
|
if (openFd.flags.append) {
|
|
pos = sh.syncHandle.getSize();
|
|
} else {
|
|
pos = position ?? openFd.position;
|
|
}
|
|
|
|
const written = sh.syncHandle.write(data, { at: pos });
|
|
|
|
if (position === null || openFd.flags.append) {
|
|
openFd.position = pos + written;
|
|
}
|
|
|
|
sh.node.size = sh.syncHandle.getSize();
|
|
sh.node.mtime = Date.now();
|
|
return written;
|
|
}
|
|
|
|
/** Get file status by fd. Sync. */
|
|
fstat(fd: number): StatResult {
|
|
const openFd = this.fdTable.get(fd);
|
|
if (!openFd) throw this.err('EBADF', `fd ${fd}`);
|
|
|
|
if (openFd.node.kind === 'directory') {
|
|
return new StatResult(false, 0, openFd.node.mtime, this.nextIno++);
|
|
}
|
|
|
|
const size = openFd.shared ? openFd.shared.syncHandle.getSize() : (openFd.node as FileNode).size;
|
|
return new StatResult(true, size, openFd.node.mtime, this.nextIno++);
|
|
}
|
|
|
|
/** Get file status by path. Sync. */
|
|
stat(path: string): StatResult {
|
|
const node = this.resolve(path);
|
|
const size = node.kind === 'file' ? node.size : 0;
|
|
return new StatResult(node.kind === 'file', size, node.mtime, this.nextIno++);
|
|
}
|
|
|
|
/** Same as stat (no symlink support). Sync. */
|
|
lstat(path: string): StatResult {
|
|
return this.stat(path);
|
|
}
|
|
|
|
/** Truncate an open file. Sync. */
|
|
ftruncate(fd: number, len: number): void {
|
|
const openFd = this.getFd(fd, 'file');
|
|
openFd.shared!.syncHandle.truncate(len);
|
|
openFd.shared!.node.size = len;
|
|
openFd.shared!.node.mtime = Date.now();
|
|
}
|
|
|
|
/** Update timestamps by fd. Sync (in-memory only). */
|
|
futimes(fd: number, _atime: number, mtime: number): void {
|
|
const openFd = this.fdTable.get(fd);
|
|
if (!openFd) throw this.err('EBADF', `fd ${fd}`);
|
|
openFd.node.mtime = mtime;
|
|
}
|
|
|
|
/** Update timestamps by path. Sync (in-memory only). */
|
|
utimes(path: string, _atime: number, mtime: number): void {
|
|
const node = this.resolve(path);
|
|
node.mtime = mtime;
|
|
}
|
|
|
|
/** Create a directory. ASYNC. */
|
|
async mkdir(path: string): Promise<void> {
|
|
const { parent, name } = this.resolveParent(path);
|
|
|
|
if (parent.children.has(name)) {
|
|
throw this.err('EEXIST', path);
|
|
}
|
|
|
|
const handle = await parent.handle.getDirectoryHandle(name, { create: true });
|
|
parent.children.set(name, {
|
|
name,
|
|
kind: 'directory',
|
|
handle,
|
|
children: new Map(),
|
|
mtime: Date.now(),
|
|
});
|
|
}
|
|
|
|
/** List directory contents. Sync (reads from in-memory index). */
|
|
readdir(path: string, withFileTypes: boolean): any[] {
|
|
const node = this.resolve(path);
|
|
if (node.kind !== 'directory') throw this.err('ENOTDIR', path);
|
|
|
|
if (withFileTypes) {
|
|
return Array.from(node.children.values()).map(child => ({
|
|
name: child.name,
|
|
isFile: () => child.kind === 'file',
|
|
isDirectory: () => child.kind === 'directory',
|
|
isSymbolicLink: () => false,
|
|
isCharacterDevice: () => false,
|
|
isBlockDevice: () => false,
|
|
isSocket: () => false,
|
|
isFIFO: () => false,
|
|
}));
|
|
}
|
|
return Array.from(node.children.keys());
|
|
}
|
|
|
|
/** Rename / move a file or directory. ASYNC. */
|
|
async rename(oldPath: string, newPath: string): Promise<void> {
|
|
const oldNode = this.resolve(oldPath);
|
|
const { parent: oldParent, name: oldName } = this.resolveParent(oldPath);
|
|
const { parent: newParent, name: newName } = this.resolveParent(newPath);
|
|
|
|
// Reject rename while file is open (SyncAccessHandle holds a lock)
|
|
const normOld = this.normalizePath(oldPath);
|
|
if (this.openHandles.has(normOld)) {
|
|
throw this.err('EBUSY', oldPath);
|
|
}
|
|
|
|
// Remove existing target if present
|
|
if (newParent.children.has(newName)) {
|
|
const existing = newParent.children.get(newName)!;
|
|
if (existing.kind === 'directory' && existing.children.size > 0) {
|
|
throw this.err('ENOTEMPTY', newPath);
|
|
}
|
|
await newParent.handle.removeEntry(newName, { recursive: false });
|
|
newParent.children.delete(newName);
|
|
}
|
|
|
|
// Use move() API if available (Chrome 110+, Firefox 129+)
|
|
if (typeof (oldNode.handle as any).move === 'function') {
|
|
await (oldNode.handle as any).move(newParent.handle, newName);
|
|
oldParent.children.delete(oldName);
|
|
oldNode.name = newName;
|
|
newParent.children.set(newName, oldNode);
|
|
} else if (oldNode.kind === 'file') {
|
|
// Fallback: read content, create new file, delete old
|
|
const file = await oldNode.handle.getFile();
|
|
const content = new Uint8Array(await file.arrayBuffer());
|
|
|
|
const newHandle = await newParent.handle.getFileHandle(newName, { create: true });
|
|
const syncHandle = await newHandle.createSyncAccessHandle();
|
|
syncHandle.write(content);
|
|
syncHandle.flush();
|
|
const newSize = syncHandle.getSize();
|
|
syncHandle.close();
|
|
|
|
await oldParent.handle.removeEntry(oldName);
|
|
oldParent.children.delete(oldName);
|
|
|
|
newParent.children.set(newName, {
|
|
name: newName,
|
|
kind: 'file',
|
|
handle: newHandle,
|
|
size: newSize,
|
|
mtime: Date.now(),
|
|
});
|
|
} else {
|
|
throw this.err('ENOSYS', 'directory rename requires move() API');
|
|
}
|
|
}
|
|
|
|
/** Remove an empty directory. ASYNC. */
|
|
async rmdir(path: string): Promise<void> {
|
|
const { parent, name } = this.resolveParent(path);
|
|
const child = parent.children.get(name);
|
|
if (!child) throw this.err('ENOENT', path);
|
|
if (child.kind !== 'directory') throw this.err('ENOTDIR', path);
|
|
if (child.children.size > 0) throw this.err('ENOTEMPTY', path);
|
|
|
|
await parent.handle.removeEntry(name);
|
|
parent.children.delete(name);
|
|
}
|
|
|
|
/** Delete a file. ASYNC. */
|
|
async unlink(path: string): Promise<void> {
|
|
const { parent, name } = this.resolveParent(path);
|
|
const child = parent.children.get(name);
|
|
if (!child) throw this.err('ENOENT', path);
|
|
if (child.kind === 'directory') throw this.err('EISDIR', path);
|
|
|
|
// Force-close any open SyncAccessHandle for this file
|
|
const normPath = this.normalizePath(path);
|
|
const existing = this.openHandles.get(normPath);
|
|
if (existing) {
|
|
existing.syncHandle.close();
|
|
this.openHandles.delete(normPath);
|
|
}
|
|
|
|
await parent.handle.removeEntry(name);
|
|
parent.children.delete(name);
|
|
}
|
|
|
|
// Unsupported (OPFS has no symlinks / hardlinks)
|
|
link(): never { throw this.err('ENOSYS', 'hardlinks not supported'); }
|
|
symlink(): never { throw this.err('ENOSYS', 'symlinks not supported'); }
|
|
readlink(): never { throw this.err('ENOSYS', 'symlinks not supported'); }
|
|
|
|
/** Normalize path and verify it exists. Sync. */
|
|
realpath(path: string): string {
|
|
this.resolve(path);
|
|
return this.normalizePath(path);
|
|
}
|
|
|
|
/** Flush file data to OPFS. Sync. */
|
|
fdatasync(fd: number): void {
|
|
const openFd = this.getFd(fd, 'file');
|
|
openFd.shared!.syncHandle.flush();
|
|
}
|
|
|
|
/** Flush file data + metadata. Sync. */
|
|
fsync(fd: number): void {
|
|
this.fdatasync(fd);
|
|
}
|
|
|
|
// --- Bulk operations (for main-thread UI) ---
|
|
|
|
/** Recursively delete all content under the OPFS root. ASYNC. */
|
|
async wipe(): Promise<void> {
|
|
if (!this.root) return;
|
|
this.closeAll();
|
|
await this.clearDir(this.root);
|
|
}
|
|
|
|
/** Write a file into the OPFS tree, creating parent directories as needed. ASYNC. */
|
|
async writeFile(path: string, data: Uint8Array): Promise<void> {
|
|
// Ensure parent directories exist
|
|
const segments = path.split('/').filter(Boolean);
|
|
let dir = this.root!;
|
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
const seg = segments[i];
|
|
let child = dir.children.get(seg);
|
|
if (!child) {
|
|
const handle = await dir.handle.getDirectoryHandle(seg, { create: true });
|
|
child = { name: seg, kind: 'directory', handle, children: new Map(), mtime: Date.now() };
|
|
dir.children.set(seg, child);
|
|
}
|
|
if (child.kind !== 'directory') throw this.err('ENOTDIR', path);
|
|
dir = child;
|
|
}
|
|
|
|
const fileName = segments[segments.length - 1];
|
|
let fileNode = dir.children.get(fileName);
|
|
let fileHandle: FileSystemFileHandle;
|
|
|
|
if (fileNode && fileNode.kind === 'file') {
|
|
fileHandle = fileNode.handle;
|
|
} else {
|
|
fileHandle = await dir.handle.getFileHandle(fileName, { create: true });
|
|
}
|
|
|
|
const syncHandle = await fileHandle.createSyncAccessHandle();
|
|
syncHandle.truncate(0);
|
|
syncHandle.write(data);
|
|
syncHandle.flush();
|
|
const size = syncHandle.getSize();
|
|
syncHandle.close();
|
|
|
|
dir.children.set(fileName, {
|
|
name: fileName,
|
|
kind: 'file',
|
|
handle: fileHandle,
|
|
size,
|
|
mtime: Date.now(),
|
|
});
|
|
}
|
|
|
|
/** Read a file's full contents. ASYNC. */
|
|
async readFile(path: string): Promise<Uint8Array> {
|
|
const node = this.resolve(path);
|
|
if (node.kind !== 'file') throw this.err('EISDIR', path);
|
|
|
|
const file = await node.handle.getFile();
|
|
return new Uint8Array(await file.arrayBuffer());
|
|
}
|
|
|
|
/** Recursively list all file paths under a directory. Sync. */
|
|
listFiles(basePath = '/'): string[] {
|
|
const result: string[] = [];
|
|
const node = this.resolve(basePath);
|
|
if (node.kind === 'directory') {
|
|
this.listFilesRec(basePath, node, result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// --- Private helpers ---
|
|
|
|
private async createFile(path: string): Promise<FileNode> {
|
|
const { parent, name } = this.resolveParent(path);
|
|
const handle = await parent.handle.getFileHandle(name, { create: true });
|
|
const node: FileNode = {
|
|
name,
|
|
kind: 'file',
|
|
handle,
|
|
size: 0,
|
|
mtime: Date.now(),
|
|
};
|
|
parent.children.set(name, node);
|
|
return node;
|
|
}
|
|
|
|
private async clearDir(dir: DirNode): Promise<void> {
|
|
const names = Array.from(dir.children.keys());
|
|
for (const name of names) {
|
|
await dir.handle.removeEntry(name, { recursive: true });
|
|
}
|
|
dir.children.clear();
|
|
}
|
|
|
|
private listFilesRec(basePath: string, dir: DirNode, result: string[]): void {
|
|
for (const child of dir.children.values()) {
|
|
const childPath = basePath === '/' ? `/${child.name}` : `${basePath}/${child.name}`;
|
|
if (child.kind === 'directory') {
|
|
this.listFilesRec(childPath, child, result);
|
|
} else {
|
|
result.push(childPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Get an open fd, throwing EBADF or EISDIR as appropriate. */
|
|
private getFd(fd: number, expect: 'file'): OpenFd & { shared: SharedHandle };
|
|
private getFd(fd: number, expect?: string): OpenFd;
|
|
private getFd(fd: number, expect?: string): OpenFd {
|
|
const openFd = this.fdTable.get(fd);
|
|
if (!openFd) throw this.err('EBADF', `fd ${fd}`);
|
|
if (expect === 'file' && !openFd.shared) throw this.err('EISDIR', `fd ${fd}`);
|
|
return openFd;
|
|
}
|
|
|
|
private err(code: string, detail: string): Error {
|
|
const e = new Error(`${code}: ${detail}`);
|
|
(e as any).code = code;
|
|
return e;
|
|
}
|
|
}
|