Skip to content
Standard library

velt/fs#

import { readFile, writeFile, … } from "velt/fs". File system access, with Node's names, behavior and error messages. Every operation has an async form, which runs on the runtime's blocking pool so other tasks keep running, and a *Sync form, which blocks the calling thread. Failures throw IoError.

ts
import { writeFile, readFile, mkdir, readDir, stat, remove, existsSync } from "velt/fs";
import { tmpdir } from "velt/os";

const dir = `${tmpdir()}/velt-doc-fs`;
await mkdir(dir, { recursive: true });
await writeFile(`${dir}/hello.txt`, "hi\n");
console.log(JSON.stringify(await readFile(`${dir}/hello.txt`)), (await stat(`${dir}/hello.txt`)).size); // "hi\n" 3
console.log(await readDir(dir)); // [ 'hello.txt' ]
try {
  await readFile(`${dir}/missing.txt`);
} catch (e) {
  console.log(e.code); // ENOENT
}
await remove(dir, { recursive: true });
console.log(existsSync(dir)); // false

Files#

  • readFile(path): Promise<string>: the file as UTF-8 text (EILSEQ if it isn't valid UTF-8). readFileBytes(path): Promise<u8[]>: the bytes.
  • writeFile(path, data: string) creates or truncates the file. appendFile(path, data) adds to its end.
  • copyFile(from, to), rename(from, to).
  • exists(path): Promise<bool> never throws.

For files too large to hold in memory, or written a piece at a time, use file streams (openRead, openWrite).

Directories#

  • mkdir(path, { recursive? }): with recursive, creates the parents too and succeeds when the directory exists.
  • remove(path, { recursive? }): a file, an empty directory, or with recursive a whole tree. A symlink is removed, never what it points to.
  • readDir(path): Promise<string[]>: the entry names, sorted.
  • readDirEntries(path): Promise<Dirent[]>: the entries with their types, sorted by name (Node's readdir(path, { withFileTypes: true })). A Dirent has name, isFile(), isDirectory() and isSymbolicLink(). The type comes with the listing, so a directory walk needs no stat per entry. A symlink is isSymbolicLink() and nothing else, as in Node: stat it to see what it points to.

A directory walk that skips symlinks, as find and ripgrep do by default:

ts
import { readDirEntriesSync } from "velt/fs";

function walk(dir: string, out: string[]) {
  for (const e of readDirEntriesSync(dir)) {
    const path = `${dir}/${e.name}`;
    if (e.isDirectory()) {
      walk(path, out);
    } else if (e.isFile()) {
      out.push(path);
    }
  }
}

const files: string[] = [];
walk(".", files);
console.log(files.length > 0); // true
  • stat(path): Promise<Stats> follows symlinks; lstat(path) describes a symlink itself. Stats has Node's shape: size: u64, mtimeMs: f64, isFile(), isDirectory(), isSymbolicLink() (only ever true from lstat).
  • readlink(path): Promise<string>: a symlink's target as stored in it (EINVAL if path is not a symlink).
  • symlink(target, path) creates a symlink at path; a relative target is relative to the link's directory. On Windows the link is a directory link when target is a directory, and a relative target's / separators are stored as \, both as in Node; creating links there needs Developer Mode or administrator rights (EACCES otherwise).

Sync variants#

readFileSync readFileBytesSync writeFileSync appendFileSync readDirSync readDirEntriesSync statSync lstatSync readlinkSync symlinkSync mkdirSync removeSync renameSync copyFileSync existsSync take the same arguments and return the value instead of a promise. Use them in scripts and at startup; in a server, the async forms keep other requests running.

Errors#

A failed call throws an IoError whose code is the errno's name and whose message is Node's: the code, the operating system's description, the system call and the path (ENOENT: no such file or directory, open 'data.txt'). See velt/io for the codes.

ts
import { readFileSync } from "velt/fs";
import { IoError } from "velt/io";

try {
  readFileSync("config.json");
} catch (e) {
  if (e instanceof IoError && e.code == "ENOENT") {
    console.log("no config, using defaults");
  }
}

File descriptors#

Node's file-descriptor API, for reading and writing at positions in a file (a database page, an index), written in Velt over system functions: each call is one call of the C library, with no runtime in between. A program that does not use it does not compile it. It is available on macOS and Linux; on Windows and WebAssembly a call of one of these functions is a compile error at that call, naming the system function the target lacks.

ts
import { openSync, readSync, writeSync, fsyncSync, fstatSync, closeSync, unlinkSync } from "velt/fs";
import { tmpdir } from "velt/os";

const path = `${tmpdir()}/velt-doc-fd.bin`;
const fd = openSync(path, "w+");
const page: u8[] = Buffer.alloc(4096);
page.write("page one");
writeSync(fd, page, 0, 4096, 0);
writeSync(fd, page, 0, 4096, 8192); // a hole of zeros in between
fsyncSync(fd);
const back: u8[] = Buffer.alloc(8);
console.log(readSync(fd, back, 0, 8, 8192), back.utf8Slice(0, 8)); // 8 page one
console.log(fstatSync(fd).size); // 12288
closeSync(fd);
unlinkSync(path);
  • openSync(path, flags = "r", mode = 0o666): number: a file descriptor. flags is one of Node's strings ("r", "r+", "rs", "rs+", "w", "wx", "w+", "wx+", "a", "ax", "as", "a+", "ax+", "as+") or the system's O_* bits as a number. The descriptor is closed on exec, as in Node.
  • readSync(fd, buffer, offset = 0, length?, position?): number: reads up to length bytes (default: the rest of the buffer) into buffer from offset on, at position in the file (pread), or at the file's own position, which advances, when position is null or -1. buffer is a u8[], a Buffer, a typed array or a DataView. Returns the bytes read, 0 at the end of the file.
  • writeSync(fd, buffer, offset?, length?, position?): number: writes the bytes of a u8[], a typed array or a DataView, or a string as UTF-8 (its third argument is then the position), at position or at the file's own position (at the end for a file opened to append). Returns the bytes written.
  • fsyncSync(fd) and fdatasyncSync(fd) flush the file to the disk (on macOS with F_FULLFSYNC, falling back as Node does). ftruncateSync(fd, len = 0). closeSync(fd).
  • fstatSync(fd): Stats. unlinkSync(path) removes a file.

Arguments are coerced as Node coerces them. They are positional: Node's (fd, buffer, options) form is not supported, so readSync(fd, buf, 4) reads from offset 4 to the end where Node throws a TypeError. Errors are Node's, message for message: an IoError with the errno's name as code (EBADF: bad file descriptor, read), a RangeError for an offset, length, position or descriptor out of range, and a TypeError for unknown flags. EINTR is retried.

A call costs the system call plus Node's argument checks: a pread declared directly costs the same instructions as Rust's FileExt::read_at, and readSync's checks add about 22 per call.

Edit this page on GitHub