Reference

std/fs/lib

std/fs/src/lib.trb

The file system: File, which is both a stream's reading end and its writing end, and IoError, what every operation here fails with. The whole-file helpers (readText, writeText, readBytes, writeBytes) are for what fits in memory; chunks(), lines() and the Sink side are for everything else (docs/design/STREAMS.md).

Around the contents: remove, rename, copy, createDirectory, removeDirectory, list and walk for the tree, metadata for what a path is, symbolic links, temporary files and directories, and writeBytesAtomically for a file that is replaced whole or not at all. Every one of them takes the same path text on every system - / between the names, a drive letter on Windows - and the runtime converts it where it calls the system (docs/design/PATH.md section 6), so nothing here branches on the operating system.

type IoError

type IoError with Show, Error

What went wrong with which path.

field path

path: String

The path the operation was about.

field message

message: String

What went wrong, in the operating system's or the decoder's own words.

fn show

fn show(): String

{path}: {message}.

extend IoError with From<Utf8Error>

extend IoError with From<Utf8Error>

Bytes off a disk are not always UTF-8, and a String always is - so decoding is one of the ways reading fails.

fn from

static fn from(value: Utf8Error): IoError

type File

native shared type File with Close, Sink<Bytes, IoError>

A file that is open. It has an identity (the handle of the operating system), so it is a shared type:

fn firstText(path: String): Result<String, IoError> {
  using file = File.open(path)?
  file.readAll()
}

The file closes when the last reference to it goes away - here at the end of firstText, because using keeps the name inside its block. Nothing calls close() by hand.

An open file is both ends of a stream: chunks()/lines() read it, and the file is a Sink<Bytes, IoError> for writing, so everything that writes into a sink writes into a file. Most code needs neither: the functions without self read and write whole files, and move, copy, remove and describe what a path names.

Related

  • Source - what chunks() and lines() answer.
  • Sink - the trait a File implements for writing.

fn open

native static fn open(path: String): Result<File, IoError>

Opens an existing file for reading.

fn create

native static fn create(path: String): Result<File, IoError>

Creates the file, or empties it if it is there. The counterpart of open, for the writing side.

fn readAll

native var fn readAll(): Result<String, IoError>

The rest of the file as text, from wherever the cursor is. Fails if the bytes are not UTF-8.

fn close

native var fn close()

Releases the operating system handle. Synchronous and cannot fail, as every Close promises.

fn chunks

var fn chunks(size: Int = 65536): Source<Bytes, IoError>

The file as a stream of byte chunks of at most size, each read on a thread of the blocking pool so the worker goes on meanwhile. Everything above it (lines, framing, decoding) is a Stage and therefore ordinary TorbScript.

fn lines

var fn lines(): Source<String, IoError>

The lines of the file, as a stream. A read failure ends the stream and the reader sees it, because it is in the type - it can never be swallowed halfway through.

fn add

var fn add(item: Bytes): Task<Result<Void, IoError>>

The writing end. A File is a Sink<Bytes, IoError>, so addText, addLine and fill are all there.

fn end

var fn end(): Task<Result<Void, IoError>>

Flushes to the operating system and reports what a buffered write could only report now.

fn readText

native static fn readText(path: String): Result<String, IoError>

A String is always valid UTF-8, so bytes that are not are an IoError and never a replacement character.

fn writeText

native static fn writeText(path: String, text: String): Result<Void, IoError>

Creates or empties path and writes text into it in one call - the writing counterpart of readText.

fn readBytes

static fn readBytes(path: String): Result<Bytes, IoError>

The whole file as bytes, synchronously: for what fits in memory, and for code that cannot wait for a task - a tool, a test, the top level of a program. read is the same for a task, and chunks() reads a file of any size.

Examples

fn sizeOf(path: String): Result<Int, IoError> {
  const bytes = File.readBytes(path)?
  Ok bytes.length()
}

fn writeBytes

static fn writeBytes(path: String, bytes: Bytes): Result<Void, IoError>

Creates or empties path and writes bytes into it in one call, synchronously - the counterpart of readBytes.

fn read

static fn read(path: String): Task<Result<Bytes, IoError>>

The whole file as bytes, read chunk by chunk on a thread of the blocking pool so the worker goes on meanwhile: what readBytes is for a task. The counterpart of write, which takes a source: Source.from([bytes]) is one.

fn write

static fn write(path: String, var source: Source<Bytes, IoError>): Task<Result<Void, IoError>>

Creates path and writes everything source delivers into it, of any size and without holding it in memory: File.write(path, response.body.mapFailure(IoError.from)).await()?

fn writeBytesAtomically

static fn writeBytesAtomically(path: String, bytes: Bytes): Result<Void, IoError>

Replaces path whole or not at all: the bytes go into a new file beside it, which is flushed to the disk and then moved over path in one step. A reader - or the program after a crash - finds the old content or the new one and never a part of either, which writeBytes cannot promise. A file that was there keeps its permissions, and where this fails nothing is left behind.

Examples

fn saveIndex(path: String, entries: List<String>): Result<Void, IoError> {
  File.writeTextAtomically path, entries.joined(separator: "\n")
}

Pitfalls

  • On Windows a file that another program holds open without allowing its deletion cannot be replaced, and the call fails; the old content stays.

fn writeTextAtomically

static fn writeTextAtomically(path: String, text: String): Result<Void, IoError>

File.writeBytesAtomically for text, which is always UTF-8.

fn absolutePath

native static fn absolutePath(path: String): Result<String, IoError>

The path as an absolute path, with . and .. resolved against the working directory. The file does not have to exist: this is text arithmetic, not a lookup, so it does not follow links either.

fn exists

native static fn exists(path: String): Bool

Whether something exists at path, file or directory. A symbolic link counts as what it points at.

fn isDirectory

native static fn isDirectory(path: String): Bool

Whether path is a directory rather than a file.

fn createDirectory

native static fn createDirectory(path: String): Result<Void, IoError>

Creates the directory and every directory above it that is missing, and does nothing where it is already there - so a caller that only wants a place to write does not have to ask first.

fn list

native static fn list(path: String): Result<List<String>, IoError>

The names of the entries of a directory, sorted.

Errors

  • The directory cannot be read, or the name of one of its entries is not valid UTF-8. A String is always UTF-8, so such an entry has no name to answer: the message shows its bytes, and no replacement character is invented.

fn walk

static fn walk(path: String): Result<List<String>, IoError>

Every entry below the directory, as a path relative to it with / between the names: a directory comes before what is in it, and the entries of one directory come in the order list answers them. A symbolic link is an entry and is never followed, so a link that points back up cannot make the walk endless.

Examples

fn sources(root: String): Result<List<String>, IoError> {
  const entries = File.walk(root)?
  Ok entries.filter({ _.endsWith ".trb" }).toList()
}

fn remove

native static fn remove(path: String): Result<Void, IoError>

Removes one file, one symbolic link - the link, never what it points at - or one empty directory. A read-only file is removed as well, on Windows too. Fails where nothing is there.

fn removeDirectory

static fn removeDirectory(path: String): Result<Void, IoError>

Removes the directory and everything in it, and does nothing where nothing is there - the counterpart of createDirectory, which does nothing where the directory is already there. A symbolic link inside is removed and never followed, so nothing outside the directory is touched; path itself being a link removes the link.

Errors

  • path is a file, or something inside cannot be removed. What was removed before that stays removed.

fn rename

native static fn rename(path: String, to: String): Result<Void, IoError>

Moves path to to in one step, replacing a file that is at to - which is what makes it the last step of every replacement that must not be seen half done. Both have to be on one volume: across volumes a move is a copy and a remove, and no system does that in one step.

fn copy

native static fn copy(path: String, to: String): Result<Void, IoError>

Copies the content and the permissions of the file path to to, which is created or replaced. A symbolic link is followed; a directory is refused.

fn metadata

static fn metadata(path: String): Result<Metadata, IoError>

What path is, how large, when it was last written and who may do what with it - of what a symbolic link points at, the way every other function here follows one. linkMetadata describes the link itself.

Examples

fn isNewer(first: String, second: String): Result<Bool, IoError> {
  const one = File.metadata(first)?
  const other = File.metadata(second)?
  Ok(one.modified > other.modified)
}

fn linkMetadata

static fn linkMetadata(path: String): Result<Metadata, IoError>

File.metadata of a symbolic link itself rather than of what it points at: the one way to see that a path is a link.

fn setPermissions

static fn setPermissions(path: String, permissions: Permissions): Result<Void, IoError>

Sets the permission bits of path. On Windows only the owner's write bit counts: without it the file is read-only, and with it it is not.

fn symbolicLinkTarget

native static fn symbolicLinkTarget(path: String): Result<String, IoError>

The target of the symbolic link at path as it was stored, with / between its names on every system.

fn createTemporaryFile

static fn createTemporaryFile(prefix: String = "", inside: String? = None): Result<String, IoError>

Creates a new, empty file with a name nobody else has - prefix and twelve letters and digits - and answers its path. It is created where nothing was, so no other program can have it too, and on POSIX only its owner may read it. It lives in inside, or in the system's directory for temporary files; removing it is the caller's.

Examples

fn scratch(content: String): Result<String, IoError> {
  const path = File.createTemporaryFile(prefix: "report-")?
  File.writeText(path, content)?
  Ok path
}

fn createTemporaryDirectory

static fn createTemporaryDirectory(prefix: String = "", inside: String? = None): Result<String, IoError>

Creates a new, empty directory with a name nobody else has, the way File.createTemporaryFile creates a file, and answers its path. removeDirectory removes it with everything that was put into it.