fs
js/file/fs.ts
fino:file — POSIX filesystem with async I/O and a virtualizable handle model.
This module provides file and directory access via libc FFI. It exposes a
DiskFileSystem class that wraps every relevant POSIX syscall: open(2),
read(2), write(2), stat(2), readdir(3), rename(2), symlink(2),
etc. The I/O is wired to the event loop so that reads and writes yield
control to other async tasks while waiting for the kernel.
Design: explicit filesystem instance
Unlike Node.js's implicit global fs module, here callers construct a
DiskFileSystem explicitly:
const fs = new DiskFileSystem();
This is intentional. It keeps the backend swappable behind the abstract
FileSystem interface — enabling alternative backends (in-memory, zip
archive, overlay) — and avoids shared global state that makes testing
harder.
Object hierarchy
DiskFileSystem — the factory; owns no fds itself
.open() → File — an open fd; owns the fd lifecycle
.reader() → async iterable of Uint8Array chunks
.writer() → Writer (from fino:stream)
.bytes() → Promise<Uint8Array> (reads entire file)
.text() → Promise<string>
.dir() → DirEntry — directory handle (uses opendir/readdir/closedir)
.entries() → Promise<Entry[]>
[Symbol.asyncIterator] — iterates entries
.entry() → Entry / FileEntry / DirEntry
File owns its fd and closes it on file.close(). The Reader/Writer
produced by file.reader() / file.writer() borrow the fd with a no-op
onClose callback — do not close the Reader/Writer to release the fd; call
file.close() instead.
Constants
const F_OK
Access flag that checks whether a path exists.
Pass this to DiskFileSystem.access() when existence is the only required
condition. This is the default mode.
import { DiskFileSystem, F_OK } from 'fino:file';
const fs = new DiskFileSystem();
await fs.access('/tmp/app.log', F_OK);
const R_OK
Access flag that checks read permission for the current process.
Combine with W_OK or X_OK using bitwise OR when multiple permissions are
required.
import { DiskFileSystem, R_OK } from 'fino:file';
const fs = new DiskFileSystem();
await fs.access('/tmp/app.log', R_OK);
const W_OK
Access flag that checks write permission for the current process.
The result reflects the process credentials and platform access(2)
behavior; it is not a guarantee that a later write will succeed.
import { DiskFileSystem, W_OK } from 'fino:file';
const fs = new DiskFileSystem();
await fs.access('/tmp/app.log', W_OK);
const X_OK
Access flag that checks execute/search permission for the current process.
For directories this checks search permission. For files this checks executable permission according to the platform.
import { DiskFileSystem, X_OK } from 'fino:file';
const fs = new DiskFileSystem();
await fs.access('/tmp/script.sh', X_OK);
Classes
class DiskFileSystem extends FileSystem {
A POSIX filesystem backend backed by libc syscalls via FFI.
Each method accepts either a raw path string or a Path instance. Methods
throw errno-backed errors when the underlying syscall fails; they do not
return null for missing paths unless documented by a lower-level handle
API. File handles returned from open() must be closed by the caller.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
const text = new TextDecoder().decode(await fs.readFile('/etc/hosts'));
Methods
async stat(path: Path | string): Promise<Stat>
Stat a path, following symlinks.
Returns parsed POSIX metadata for the target. If path is a symlink, the
returned Stat describes the symlink target. Throws when the path cannot
be resolved or the process lacks permission.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
const stat = await fs.stat('/tmp/app.log');
console.log(stat.isFile(), stat.size);
async lstat(path: Path | string): Promise<Stat>
Stat a path without following symlinks.
Returns metadata for the directory entry itself. For symlinks, this
describes the link rather than the linked target. Throws on missing paths,
permission failures, or other lstat(2) errors.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
const stat = await fs.lstat('/tmp/current');
console.log(stat.isSymlink());
async open(path: Path | string, mode: string = 'r'): Promise<File>
Open a file and return a File handle.
The default mode is 'r'. Supported mode strings are 'r', 'w', 'a',
'r+', 'w+', 'a+', and 'c+' (read/write, create without truncating);
unknown mode strings throw. Create modes use 0o666 before the process
umask and then normalize new files to 0o644. Throws if the file cannot be
opened. Close the returned File when finished — the readers and writers
it produces borrow its fd rather than owning it.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
const file = await fs.open('/tmp/out.txt', 'w');
try {
await file.writer().write(new TextEncoder().encode('hello'));
} finally {
await file.close();
}
async dir(path: Path | string): Promise<DirEntry>
Open a directory and return a DirEntry handle. Throws if the path does not refer to a directory.
The returned entry can enumerate children with entries() or async
iteration. Symlinks are not followed for the directory check.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
const dir = await fs.dir('/tmp');
for (const entry of await dir.entries()) console.log(entry.name);
async entry(path: Path | string): Promise<Entry>
Construct an Entry (FileEntry / DirEntry / Entry) for any path using lstat.
Directories become DirEntry, regular files become FileEntry, symlinks
become a generic Entry with link type, and other filesystem nodes become
a generic Entry with unknown type. Throws if path cannot be lstat'ed.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
const entry = await fs.entry('/tmp/app.log');
console.log(entry.name, entry.isFile());
async mkdir(path: Path | string, mode: number = 493): Promise<void>
Create a directory.
Creates exactly one directory. Parent directories are not created
automatically. The default mode is 0o755 before the process umask.
Throws if the path exists, a parent is missing, or permissions fail.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.mkdir('/tmp/fino-cache', 0o700);
async rmdir(path: Path | string): Promise<void>
Remove an empty directory.
This wraps rmdir(2), so it only succeeds for empty directories. It
throws if the path is not a directory, is not empty, is missing, or cannot
be removed.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.rmdir('/tmp/empty-cache');
async unlink(path: Path | string): Promise<void>
Delete a file.
Removes a directory entry with unlink(2). For symlinks, the link itself
is removed and the target is left untouched. Throws for directories,
missing paths, or permission failures.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.unlink('/tmp/output.tmp');
async chmod(path: Path | string, mode: number): Promise<void>
Change the permissions of a file.
Follows symlinks, matching chmod(2). Throws when the target is missing or
the process cannot change permissions.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.chmod('/tmp/run.sh', 0o755);
async chown(path: Path | string, uid: number, gid: number): Promise<void>
Change the owner and group of a file, following symlinks.
Pass numeric user and group IDs. This follows symlinks and usually requires elevated privileges. Throws for missing paths, invalid IDs, or permission failures.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.chown('/tmp/app.log', 501, 20);
async lchown(path: Path | string, uid: number, gid: number): Promise<void>
Change the owner and group of a file without following symlinks.
For symlinks, changes ownership of the link itself. The same permission
and platform caveats as lchown(2) apply.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.lchown('/tmp/current-link', 501, 20);
async utimes(path: Path | string, atime: Date | number, mtime: Date | number): Promise<void>
Set the access and modification times of a file.
Numeric timestamps are interpreted as seconds since the Unix epoch. Date
values are converted to fractional seconds. Throws when the target is
missing or timestamp updates are not permitted.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.utimes('/tmp/app.log', new Date(), new Date());
async truncate(path: Path | string, size = 0): Promise<void>
Truncate a file to a specified length.
The default size is 0, which empties the file. Growing a file may create
sparse zero-filled space depending on the filesystem. Throws if the path is
missing, not writable, or invalid for truncation.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.truncate('/tmp/app.log');
async link(existingPath: Path | string, newPath: Path | string): Promise<void>
Create a hard link.
Creates newPath as another directory entry for existingPath. The source
and destination must usually be on the same filesystem. Throws when the
target exists, the source is missing, or hard links are not allowed.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.link('/tmp/report.txt', '/tmp/report-copy.txt');
async access(path: Path | string, mode = F_OK): Promise<void>
Test access to a path.
Wraps access(2). The default mode is F_OK, which only checks
existence. Combine R_OK, W_OK, and X_OK to check permissions from
the process perspective. Throws when the requested access is unavailable.
import { DiskFileSystem, R_OK, W_OK } from 'fino:file';
const fs = new DiskFileSystem();
await fs.access('/tmp/app.log', R_OK | W_OK);
async copyFile(src: Path | string, dest: Path | string): Promise<void>
Copy a file, preserving permissions.
Reads the entire source file into memory, writes the destination with truncation, then applies the source mode bits. This is intended for modest files; stream manually for very large files. Throws if either open, read, write, or chmod step fails.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.copyFile('/tmp/input.txt', '/tmp/output.txt');
async rename(oldPath: Path | string, newPath: Path | string): Promise<void>
Rename or move a file or directory.
Wraps rename(2). Existing destination behavior follows the host POSIX
rules. Moving across filesystems may fail. Throws on missing sources,
invalid destinations, or permission errors.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.rename('/tmp/upload.tmp', '/tmp/upload.txt');
async readlink(path: Path | string): Promise<string>
Read the target of a symbolic link.
Returns the raw link target string exactly as stored by the symlink. The
target may be relative and may not exist. Throws if path is not a symlink
or cannot be read.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
console.log(await fs.readlink('/tmp/current'));
async symlink(target: Path | string, linkpath: Path | string): Promise<void>
Create a symbolic link.
The target is stored as provided; it is not required to exist and is not
normalized. Throws if linkpath already exists or the platform rejects the
link creation.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.symlink('releases/current', '/tmp/app-current');
async realpath(path: Path | string): Promise<string>
Resolve the canonical absolute path, expanding symlinks.
Wraps realpath(3) and returns a string. The path and all required
components must exist. Throws for missing components, loops, or permission
failures.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
console.log(await fs.realpath('/tmp/../tmp'));
async readFile(path: Path | string): Promise<Uint8Array>
Read an entire file and return its bytes.
Opens the file in read mode, reads all bytes, and closes the handle. This
loads the full file into memory. Text decoding is intentionally caller-owned:
pass the returned Uint8Array to TextDecoder when text is expected.
Throws on open or read-related filesystem errors.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
const bytes = await fs.readFile('/tmp/config.json');
const text = new TextDecoder().decode(bytes);
async writeFile(
path: Path | string,
data: Uint8Array | ArrayBuffer | ArrayBufferView
): Promise<void>
Write data to a file, creating or truncating it.
Opens the path with mode 'w', writes the full byte buffer, and closes the
handle. Text encoding is intentionally caller-owned: pass strings through
TextEncoder before calling this method. Parent directories are not
created. Throws on open, invalid input, or write failure.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
await fs.writeFile('/tmp/message.txt', new TextEncoder().encode('hello\n'));
glob(pattern: string, options?: GlobOptions): AsyncGenerator<Entry>
Walk the filesystem matching entries against a glob pattern.
Yields Entry / FileEntry / DirEntry objects for each match.
Pattern evaluation is delegated to the internal glob walker. Directory reads happen lazily as iteration advances. Errors from directory listing or entry inspection propagate through the async iterator.
** matches zero or more path segments, so src/** walks everything under
src/ recursively.
import { DiskFileSystem } from 'fino:file';
const fs = new DiskFileSystem();
for await (const entry of fs.glob('src/**')) {
if (entry.isFile()) console.log(entry.path.toString());
}
abstract class FileSystem {
Abstract base class for filesystem providers.
Defines the contract every provider must satisfy: the abstract methods are
the core POSIX-like primitives (stat, open, mkdir, rename, and so on)
that each backend implements against its own storage. The concrete
convenience methods readFile and writeFile are implemented here in terms
of open and the returned handle's reader/writer, so every provider inherits
them for free; a provider may override them when it can do the whole-file
transfer more efficiently.
The optional *Sync methods mirror the async primitives for native callback
integrations (such as the SQLite VFS) and can be omitted by providers that do
not need synchronous access.
import { FileSystem } from 'internal:file/provider';
class ReadOnlyFs extends FileSystem {
// Implement stat, lstat, open, dir, entry, mkdir, rmdir, unlink,
// rename, readlink, symlink, and realpath against the backing store.
}
const fs: FileSystem = new ReadOnlyFs();
const text = new TextDecoder().decode(await fs.readFile('/etc/hostname'));
console.log(text.trim());
Methods
abstract stat(path: Path | string): Promise<Stat>
Stat a path, following symlinks.
const stat = await fs.stat('/tmp/file.txt');
abstract lstat(path: Path | string): Promise<Stat>
Stat a path without following symlinks.
const stat = await fs.lstat('/tmp/link');
abstract open(path: Path | string, mode?: string): Promise<FileHandle>
Open a file and return a handle.
Mode defaults to r. Providers used by low-level integrations should also
support internal c+: read-write, create if missing, preserve if present.
Unsupported modes should throw before opening.
const file = await fs.open('/tmp/file.txt', 'r');
abstract dir(path: Path | string): Promise<unknown>
Open a directory handle. Returns a DirEntry (fino:file) or equivalent
directory object. Typed as unknown here to avoid circular dependencies
between the provider interface and the concrete Entry types.
const dir = await fs.dir('/tmp');
abstract entry(path: Path | string): Promise<unknown>
Return an Entry object for a path. Returns an Entry subtype (fino:file)
or equivalent. Typed as unknown here to avoid circular dependencies.
const entry = await fs.entry('/tmp/file.txt');
abstract mkdir(path: Path | string, mode?: number): Promise<void>
Create a directory.
Mode defaults are provider-specific; disk providers use POSIX permissions.
await fs.mkdir('/tmp/new-dir', 0o755);
abstract rmdir(path: Path | string): Promise<void>
Remove an empty directory.
Throws if the directory does not exist or is not empty.
await fs.rmdir('/tmp/empty-dir');
abstract unlink(path: Path | string): Promise<void>
Delete a file or symlink.
Directory removal should use rmdir.
await fs.unlink('/tmp/file.txt');
abstract rename(oldPath: Path | string, newPath: Path | string): Promise<void>
Rename or move a file or directory.
Replacement behavior follows provider semantics; disk providers use
rename(2).
await fs.rename('/tmp/a.txt', '/tmp/b.txt');
abstract readlink(path: Path | string): Promise<string>
Read the target of a symlink.
const target = await fs.readlink('/tmp/link');
abstract symlink(target: Path | string, linkpath: Path | string): Promise<void>
Create a symlink.
await fs.symlink('/tmp/target', '/tmp/link');
abstract realpath(path: Path | string): Promise<string>
Resolve the canonical absolute path.
const abs = await fs.realpath('.');
async readFile(path: Path | string): Promise<Uint8Array>
Read the entire file contents as bytes.
Opens the file in read mode and always closes the handle in a finally
block. Text decoding is intentionally caller-owned. Providers may override
this for efficiency.
const bytes = await fs.readFile('/tmp/file.txt');
async writeFile(
path: Path | string,
data: Uint8Array | ArrayBuffer | ArrayBufferView
): Promise<void>
Write byte data to a file, creating or truncating as needed.
The file is opened with mode w, flushed, and closed even when writing
fails. Text encoding is intentionally caller-owned.
await fs.writeFile('/tmp/file.txt', new TextEncoder().encode('hello'));
class Stat {
File metadata parsed from a struct stat buffer.
Numeric fields that may be large (ino, dev, size, blocks) are returned as Numbers; they fit within JS safe-integer range for all practical file sizes.
import { Stat } from 'internal:file/stat';
const stat = new Stat(1, 2, 0o100644, 1, 501, 20, 0, 12, 4096, 1, 0, 0, 0, 0);
stat.isFile(); // true
Constructors
constructor(
dev: number,
ino: number,
mode: number,
nlink: number,
uid: number,
gid: number,
rdev: number,
size: number,
blksize: number,
blocks: number,
atimeMs: number,
mtimeMs: number,
ctimeMs: number,
birthtimeMs: number
)
Create a parsed stat value.
Timestamps are milliseconds since the Unix epoch. birthtimeMs is zero on
Linux because the parsed layout does not include creation time.
import { Stat } from 'internal:file/stat';
const stat = new Stat(1, 2, 0o100644, 1, 501, 20, 0, 12, 4096, 1, 0, 0, 0, 0);
Getters
get dev()
Device ID containing the inode.
const dev = stat.dev;
get ino()
Inode number.
const ino = stat.ino;
get mode()
Raw POSIX mode bits, including file type and permissions.
const mode = stat.mode;
get nlink()
Hard-link count.
const links = stat.nlink;
get uid()
Owner user ID.
const uid = stat.uid;
get gid()
Owner group ID.
const gid = stat.gid;
get rdev()
Device ID for special files.
const rdev = stat.rdev;
get size()
File size in bytes.
const size = stat.size;
get blksize()
Preferred block size for filesystem I/O.
const blockSize = stat.blksize;
get blocks()
Allocated block count.
const blocks = stat.blocks;
get atimeMs()
Last access time in milliseconds since Unix epoch.
const atime = stat.atimeMs;
get mtimeMs()
Last modification time in milliseconds since Unix epoch.
const mtime = stat.mtimeMs;
get ctimeMs()
Last status-change time in milliseconds since Unix epoch.
const ctime = stat.ctimeMs;
get birthtimeMs()
Creation time in milliseconds since Unix epoch when available.
Linux parsing returns zero because the current struct layout does not expose birth time.
const birth = stat.birthtimeMs;
get permissions(): number
Permission bits (mode & 0o7777).
const perms = stat.permissions;
Methods
isFile(): boolean
Return true when the mode identifies a regular file.
if (stat.isFile()) void stat.size;
isDirectory(): boolean
Return true when the mode identifies a directory.
const dir = stat.isDirectory();
isSymlink(): boolean
Return true when the mode identifies a symbolic link.
const link = stat.isSymlink();
isSocket(): boolean
Return true when the mode identifies a socket.
const socket = stat.isSocket();
isFIFO(): boolean
Return true when the mode identifies a FIFO.
const fifo = stat.isFIFO();
isBlockDevice(): boolean
Return true when the mode identifies a block device.
const block = stat.isBlockDevice();
isCharacterDevice(): boolean
Return true when the mode identifies a character device.
const chr = stat.isCharacterDevice();
Static Methods
static parse(buf: ArrayBuffer | ArrayBufferView): Stat
Parse a struct stat from a 256-byte ArrayBuffer. Layout differs between macOS arm64 and Linux x86_64.
Throws only if the supplied buffer is too small for DataView reads. The
caller is responsible for passing a buffer filled by stat, lstat, or
fstat.
import { Stat } from 'internal:file/stat';
const stat = Stat.parse(new ArrayBuffer(256));
class File {
An opened file handle over a single POSIX descriptor.
Provides metadata access, whole-file reads, chunked reader and writer
factories, positional (pread/pwrite) I/O, advisory locking, syncing, and
truncation. The handle owns the descriptor lifecycle: call close() (or use
await using) exactly once when finished. Readers and writers produced by a
handle share the same underlying fd, so close the File — not the individual
stream — to release it. Closing flushes any writer created by writer().
Async reads use io_uring on Linux and yield through the runtime loop around
synchronous syscalls on macOS. Every method throws if the handle is already
closed, and reader()/writer() also throw when the open mode disallows the
requested direction.
import { File } from 'internal:file/handle';
const file = new File(fd, fs, '/tmp/log.txt', 'r+');
try {
for await (const chunk of file.reader()) {
process.stdout.write(chunk);
}
} finally {
await file.close();
}
Constructors
constructor(fd: number, fs: object, path: Path | string, mode: string)
Wrap an already-open file descriptor as a File handle.
The descriptor must already be opened by the calling provider; the
constructor takes ownership of its lifecycle but does not open or dup it.
path is normalized and kept for diagnostics only. mode gates the
high-level helpers: readable modes enable reader()/bytes()/text(),
writable modes enable writer(), and read-write modes (r+, w+, a+,
c+) additionally install split().
import { File } from 'internal:file/handle';
const file = new File(fd, fs, '/tmp/file.txt', 'r+');
const [reader, writer] = file.split!();
Getters
get path()
The path this handle was opened from.
Retained for diagnostics and error messages only; reading it never touches the filesystem or re-opens the descriptor.
console.log(`reading ${file.path.toString()}`);
get closed()
Whether close has been called on this handle.
Once true, methods that touch the descriptor throw File is closed, and a
live reader() iterator reports completion instead of yielding. Use it to
make cleanup idempotent.
if (!file.closed) await file.close();
Methods
async stat(): Promise<Stat>
Return file metadata by running fstat(2) against the open descriptor.
Reflects the current state of the file (size, mode, timestamps) even if it was renamed or unlinked after opening. Throws if the handle is closed or the syscall fails.
const stat = await file.stat();
console.log(`${stat.size} bytes, mode ${stat.mode.toString(8)}`);
reader(): AsyncIterable<Uint8Array>
Return an async iterable that streams the file in up-to-64 KiB chunks from the current offset to EOF.
Reading advances the shared file offset, so consuming the iterator, then
calling bytes() or another reader(), continues from where iteration
stopped. If the file is closed mid-iteration the iterator completes cleanly.
Throws immediately if the handle was opened in a non-readable mode (w,
a).
On Linux this issues IORING_OP_READ for genuine async I/O. On macOS it
yields through the runtime loop via kqueue EVFILT_READ between synchronous
read(2) calls, checking lseek(SEEK_CUR) against the file size before each
wait so it exits cleanly at EOF (where EVFILT_READ never fires) while still
noticing a file that has grown.
let total = 0;
for await (const chunk of file.reader()) {
total += chunk.byteLength;
}
console.log(`streamed ${total} bytes`);
writer(): FdWriter
Return a buffered FdWriter that appends to this file at the current
offset.
The writer shares the handle's descriptor, so close the File — not the
writer — to release it; close() flushes the most recently created writer
first. Valid only for writable modes (w, a, r+, w+, a+, c+);
throws otherwise. Each call installs a fresh writer as the one flushed on
close.
const writer = file.writer();
writer.write(new TextEncoder().encode('hello\n'));
await writer.flush();
async bytes(): Promise<Uint8Array>
Read the file from the current offset to EOF and return it as one
Uint8Array.
Drains the same chunked read loop as reader() and concatenates the result,
so it advances the shared file offset and returns an empty array when
already at EOF. Throws if the handle is closed. For large files prefer
reader() to avoid holding the whole contents in memory. Uses the same
Linux io_uring / macOS kqueue EOF strategy documented on reader().
const bytes = await file.bytes();
console.log(`loaded ${bytes.byteLength} bytes`);
async text(): Promise<string>
Read the file from the current offset to EOF and decode it as UTF-8 text.
A convenience wrapper over bytes() with the same offset and EOF behavior;
invalid UTF-8 is replaced with the Unicode replacement character rather than
throwing. Throws if the handle is closed.
const config = JSON.parse(await file.text());
async pread(pos: number | bigint, len: number): Promise<Uint8Array>
Read up to len bytes starting at absolute offset pos without moving the
shared file offset.
Because it is positional, pread is safe to interleave with reader() or
other positional calls on the same handle. It returns a buffer shorter than
len when pos lands near EOF and an empty buffer when len is 0. Throws
if the handle is closed or pread(2) fails.
const header = await file.pread(0n, 16);
if (header.byteLength < 16) throw new Error('file truncated');
async pwrite(pos: number | bigint, data: Uint8Array): Promise<number>
Write data at absolute offset pos without moving the shared file offset.
Returns the number of bytes actually written, which may be fewer than
data.byteLength; callers needing a complete write should loop until the
whole buffer is consumed. Writing past the current end of the file extends
it. Throws if the handle is closed or pwrite(2) fails.
let off = 0;
const data = new TextEncoder().encode('record');
while (off < data.byteLength) {
off += await file.pwrite(1024 + off, data.subarray(off));
}
async sync(): Promise<void>
Flush file data and metadata to stable storage with fsync(2).
Buffered writes go through an FdWriter, so flush the writer before calling
sync() to guarantee the bytes have reached the descriptor. Throws if the
handle is closed or the syscall fails.
const writer = file.writer();
writer.write(new Uint8Array([1, 2, 3]));
await writer.flush();
await file.sync();
async truncate(len: number | bigint): Promise<void>
Set the file to exactly len bytes with ftruncate(2).
Shrinking discards the trailing bytes; extending grows the file with a zero-filled (typically sparse) region. The shared file offset is unchanged, so it can point past the new end. Throws if the handle is closed or the syscall fails.
await file.truncate(0); // reset the file to empty
async size(): Promise<bigint>
Return the current file size in bytes as a bigint.
Queries fstat(2) on the open descriptor; the bigint return keeps the
full 64-bit range for large files and matches the provider interface. Throws
if the handle is closed or the syscall fails.
const size = await file.size();
const tail = await file.pread(size - 64n, 64);
async close(): Promise<void>
Flush any active writer, then close the handle and release the descriptor.
Idempotent: a second call is a no-op. Any pending writer created by
writer() is flushed before the fd is released, so buffered bytes are not
lost. On Linux the close is issued as IORING_OP_CLOSE; on macOS it runs
synchronously via close(2). This method also backs Symbol.asyncDispose,
so an await using binding closes the handle when it leaves scope.
await using file = new File(fd, fs, '/tmp/scratch', 'w');
file.writer().write(new Uint8Array([0]));
// handle is flushed and closed automatically at end of scope
class Entry {
Base handle for a filesystem entry. Holds the name, full path, a reference
to the parent FileSystem, and the d_type from the directory listing (used
for fast type checks without an extra stat call).
Detached entries (fs === null) can still expose name, path, and d_type
checks, but stat and lstat throw because no provider is available.
import { Entry } from 'internal:file/entry';
const entry = new Entry('file.txt', '/tmp/file.txt', null, 8);
entry.name; // 'file.txt'
Constructors
constructor(name: string, path: Path | string, fs: EntryFileSystem | null, dtype: number)
Create an entry wrapper.
dtype is the platform dirent.d_type value and may be DT_UNKNOWN when
the filesystem cannot provide a cheap type.
import { Entry } from 'internal:file/entry';
const entry = new Entry('unknown', '/tmp/unknown', null, 0);
Getters
get name()
Entry basename as reported by the directory listing.
const name = entry.name;
get path()
Full entry path as a Path instance.
const path = entry.path.toString();
Methods
isFile(): boolean
Fast type check from dirent.d_type.
No syscall is performed, so this returns false for DT_UNKNOWN even if the
path is actually a file. Use stat when accuracy is required.
const isFile = entry.isFile();
isDirectory(): boolean
Fast directory check from dirent.d_type.
const isDir = entry.isDirectory();
isSymlink(): boolean
Fast symlink check from dirent.d_type.
const isLink = entry.isSymlink();
async stat(): Promise<Stat>
Stat this entry, following symlinks.
Throws when the entry is detached from a filesystem.
const stat = await entry.stat();
async lstat(): Promise<Stat>
Lstat this entry, without following symlinks.
Throws when the entry is detached from a filesystem.
const stat = await entry.lstat();
class FileEntry extends Entry {
A filesystem entry that represents a regular file.
import { FileEntry } from 'internal:file/entry';
const fileEntry = new FileEntry('file.txt', '/tmp/file.txt', null, 8);
fileEntry.isFile(); // true
Constructors
constructor(name: string, path: Path | string, fs: EntryFileSystem | null, dtype: number)
Create a regular-file entry.
The filesystem reference is required for open; detached entries can only
be inspected.
import { FileEntry } from 'internal:file/entry';
const fileEntry = new FileEntry('file.txt', '/tmp/file.txt', null, 8);
Methods
async open(mode: string = 'r')
Open this file.
Resolves to the provider's open-file handle. Mode defaults to r and is
passed through to the owning filesystem, so the accepted mode strings are
whatever that provider supports. Throws when the entry is detached from a
filesystem, or when the provider rejects the mode or path.
const file = await fileEntry.open('r');
await file.close();
class DirEntry extends Entry {
A filesystem entry that represents a directory. Implements the async
iterator protocol so it can be used directly in for await loops.
import { DirEntry } from 'internal:file/entry';
const dir = new DirEntry('tmp', '/tmp', null, 4);
dir.isDirectory(); // true
Constructors
constructor(name: string, path: Path | string, fs: EntryFileSystem | null, dtype: number)
Create a directory entry.
The filesystem reference is required for listing children and mutating the directory.
import { DirEntry } from 'internal:file/entry';
const dir = new DirEntry('tmp', '/tmp', null, 4);
Methods
async entries(): Promise<Entry[]>
Read every child of this directory into an array, skipping . and ...
Unlike the provider-backed methods, this reads the local disk directly
through libc opendir(3)/readdir(3), decoding the platform-specific
struct dirent layout in JS. Each child is wrapped by concrete type from
its d_type: a FileEntry for regular files, a DirEntry for
directories, and a plain Entry for everything else (symlinks, sockets,
devices, or types the listing could not classify). Throws if opendir
fails, for example when the path does not exist or is not a directory.
for (const child of await dir.entries()) {
if (child instanceof FileEntry) console.log('file', child.name);
else if (child instanceof DirEntry) console.log('dir', child.name);
}
async child(name: string): Promise<Entry>
Resolve a single named child through the provider, typed by its kind.
This does not scan the directory; it delegates to the filesystem's entry
lookup (a single lstat-style probe), so it is cheap even in large
directories. The returned object is a FileEntry, DirEntry, or plain
Entry depending on what the provider reports. Throws when the entry is
detached from a filesystem, or when the provider cannot resolve the name.
const child = await dir.child('file.txt');
console.log(child.isFile());
async childFile(name: string): Promise<FileEntry>
Return a FileEntry for a named child.
Throws when the child exists but is not a regular file according to the provider entry type.
const file = await dir.childFile('file.txt');
async childDir(name: string): Promise<DirEntry>
Return a DirEntry for a named child directory.
Throws when the child exists but is not a directory according to the provider entry type.
const subdir = await dir.childDir('nested');
async mkdir(name: string, mode: number = 493): Promise<void>
Create a child directory.
Mode defaults to 0o755 for disk-backed providers.
await dir.mkdir('nested');
async remove(name: string): Promise<void>
Remove a child entry. Uses unlink for files and rmdir for directories.
The child is first inspected with lstat. Directory removal requires the
directory to be empty.
await dir.remove('old.txt');