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.
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)); // falseFiles
readFile(path): Promise<string>: the file as UTF-8 text (EILSEQif 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? }): withrecursive, creates the parents too and succeeds when the directory exists.remove(path, { recursive? }): a file, an empty directory, or withrecursivea 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'sreaddir(path, { withFileTypes: true })). ADirenthasname,isFile(),isDirectory()andisSymbolicLink(). The type comes with the listing, so a directory walk needs nostatper entry. A symlink isisSymbolicLink()and nothing else, as in Node:statit to see what it points to.
A directory walk that skips symlinks, as find and ripgrep do by default:
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); // trueMetadata and links
stat(path): Promise<Stats>follows symlinks;lstat(path)describes a symlink itself.Statshas Node's shape:size: u64,mtimeMs: f64,isFile(),isDirectory(),isSymbolicLink()(only evertruefromlstat).readlink(path): Promise<string>: a symlink's target as stored in it (EINVALifpathis not a symlink).symlink(target, path)creates a symlink atpath; a relativetargetis relative to the link's directory. On Windows the link is a directory link whentargetis a directory, and a relativetarget's/separators are stored as\, both as in Node; creating links there needs Developer Mode or administrator rights (EACCESotherwise).
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.
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.
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.flagsis one of Node's strings ("r","r+","rs","rs+","w","wx","w+","wx+","a","ax","as","a+","ax+","as+") or the system'sO_*bits as a number. The descriptor is closed onexec, as in Node.readSync(fd, buffer, offset = 0, length?, position?): number: reads up tolengthbytes (default: the rest of the buffer) intobufferfromoffseton, atpositionin the file (pread), or at the file's own position, which advances, whenpositionisnullor -1.bufferis au8[], aBuffer, a typed array or aDataView. Returns the bytes read, 0 at the end of the file.writeSync(fd, buffer, offset?, length?, position?): number: writes the bytes of au8[], a typed array or aDataView, or a string as UTF-8 (its third argument is then the position), atpositionor at the file's own position (at the end for a file opened to append). Returns the bytes written.fsyncSync(fd)andfdatasyncSync(fd)flush the file to the disk (on macOS withF_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.