Skip to main content
machines.fs works on a machine’s files from your code the way you work on local ones. Every call runs as the machine’s user and sees the same files its commands see, so a file written here is there for the next exec, and the other way round.
The first argument is the machine’s name or id. A sleeping machine is woken first, so there is no need to start it. Isolated machines work too. For copying files and folders from the command line or the console, see File operations.

Every call

The async Python client (AsyncBoxd) has the same calls, awaited, and its write also takes an async iterable.

Paths

A path is either absolute, or relative to the machine user’s home directory, /home/boxd: There is no shell in between, so nothing is expanded: ~ and $HOME are ordinary characters in a name, and "~/notes.md" means a directory called ~ inside the home directory.

Writing files

write replaces the whole file. Text is written as UTF-8. Missing parent directories are created, with mode 0755.
What happens to the file’s metadata:
  • A new file gets mode 0644, or mode if you pass one, and belongs to the machine’s user.
  • An existing file keeps its mode and owner. Pass mode to change the mode.
  • A symlink is written through: the link stays a link, and the file it points to gets the new contents, keeping its own mode.
Readers of the file see either the old contents or the new ones, never a mix: the new contents are written beside the file and then swapped in. A few files can’t be swapped without changing what they are, so they are overwritten in place instead, without that guarantee: a file with several hard links, a file owned by another user, a mount point, and a file in a directory the machine’s user can’t write to. The call only succeeds once every byte has arrived. If the upload is cut off, the file is left as it was, except for a file being overwritten in place, which is then incomplete and the error says so.

Reading files

Only regular files can be read. A symlink is followed to its target.
A read that is cut off before the end of the file raises an error. It never returns a shortened file.

Large files

read and write hold the whole file in memory. For files of any size, stream instead: read_stream / readStream hands you chunks as they arrive, and write takes chunks to send as they are produced. There is no size limit either way.
Breaking out of a readStream loop early cancels the transfer. In TypeScript the browser entry (@boxd-sh/sdk/web) collects a streamed write in memory before sending it.

Entries: stat, exists, list

stat, list and write describe files with an FsEntry:
exists follows symlinks, so a symlink that leads nowhere does not exist. It is also false for a path the machine’s user isn’t allowed to look at. It still raises when the machine itself can’t be reached. list returns every entry of the directory, in no particular order, with no cap on how many. Large directories arrive in batches: iter_list / listIter gives you each entry as it arrives instead of waiting for all of them.

Directories, renames and removal

  • mkdir gives new directories mode 0755, or mode. A file already at the path is an error.
  • rename with overwrite off fails if the destination exists, instead of replacing it.
  • remove on a symlink removes the link, never what it points to, even with recursive. A directory needs recursive. The root directory is refused. A recursive removal never follows symlinks out of the tree. Disks mounted inside it are left alone, and the call then fails with cross_device once everything else is gone.
  • realpath works like Python’s os.path.realpath: the parts of the path that don’t exist yet are kept as written.

Filenames that aren’t UTF-8

On the machine, a filename is a sequence of bytes. Any name works, including ones with tabs, newlines or bytes that aren’t valid UTF-8.
  • Python decodes names the way os.fsdecode does: bytes that aren’t UTF-8 are kept as surrogate escapes, so passing entry.path back to any call reaches the same file. Every path argument also accepts bytes.
  • TypeScript decodes name and path as UTF-8 for display, replacing invalid bytes. nameBytes, pathBytes and symlinkTargetBytes hold the exact bytes, and every path argument also accepts a Uint8Array. realpathBytes is realpath as exact bytes.

Errors

A failure inside the machine raises a filesystem error that says what went wrong and names the path. Each carries a reason: In Python, every filesystem error is a MachineFilesystemError, which is both a BoxdError and an OSError, with errno, strerror and filename set the way a local failure sets them. The five common ones are also the matching builtin, so code written for local files handles them unchanged:
In TypeScript, every filesystem error is a FilesystemError. Its code is the error name Node’s own fs uses, reason is the exact reason, and path is the path the call was about. The five common ones throw a subclass of it, so you can test the class or the code:
Errors about the machine rather than the path are the SDK’s usual ones, for example NotFoundError for a machine that doesn’t exist. In Python the five common filesystem classes also extend the matching status class (MachineFileNotFoundError is a NotFoundError, MachinePermissionError a PermissionDeniedError, the other three a ConflictError), so catch the filesystem class when you need to tell a missing file from a missing machine. In TypeScript FilesystemError extends BoxdError only. See the error sections of the Python and TypeScript references.

Machines started before the filesystem API

A machine that was last started before machines.fs was released can’t answer these calls. They raise MachineAgentOutdatedError in Python and AgentOutdatedError in TypeScript, with reason agent_outdated. Rebooting the machine fixes it, and only needs doing once:
This error is not a filesystem error (not an OSError in Python, not a FilesystemError in TypeScript): nothing was attempted on the machine’s disk.