One entry returned by list!.
Files
Files beneath directory handles, and their typed terminal outcomes.
An app never names a file by an ambient path. It asks io.files() for a
directory handle and names files relative to it:
init! = App.init(
App.default.with_app_id("dev.example.notes"),
|io| {
saves = io.files().app_data!()?
Ok({ saves: saves })
},
)
update! = |model, input, _io| {
if input.devices.key_pressed(KeyS) {
saves = model.saves
Task.spawn!(input, || Saved(saves.write_text!("slot1.json", encode(model))))
}
Ok(model)
}
Where a handle can come from:
app_data!,app_config!,app_cache!: the app's private storage under the user's home, created on first use. NeedsApp.Config.with_app_id.beside_executable!: the directory the executable is in, read-only -- the app's own bundle.working_directory!andworking_directory_read!: the launch directory, when the app declaresWorkingDirectory.open_dir!andopen_dir_read!: a directory aDirectory,WorkingDirectory, orFilesAnydeclaration covers.accept_drop!andfrom_arg!: a file or directory the user dropped on the window or named as an argument, as aDesignateditem.
A Dir can write; a ReadDir cannot, and Dir.read_only narrows one to
the other. subdir narrows either to a directory beneath it. A handle is an
ordinary value: keep it in the model, pass it to a helper, capture it in a
task. Code that has the handle can access what is inside that directory,
and nothing else.
A path given to a handle is plainly relative: no leading /, no drive, no
backslash, and no empty, ., or .. component. Symbolic links beneath a
handle are not followed, and an existing link where a write would land is
not written through.
Two errors say why a target was refused, and they mean different things:
PathInvalid: the path's shape, or a link it meets, is refused. The rule is the same wherever the path would have led, so fix the path.PermissionDenied: no grant covers the target. That is a directory no declaration covers, a stub handle, a drop that is not from this cycle, or a string that is not one of the app's arguments. Declare more, or ask the user to choose the item.
Every effect here waits except subdir, read_only, dir,
accept_drop! and from_arg!, which open nothing. A
waiting effect is legal in init!, where it blocks startup, and in tasks,
where it parks the task; it is refused in update! and render!.
EntryKind : [File, Dir, Other]
The filesystem kind relevant to a non-recursive directory walk.
ReadTextError : [PermissionDenied, PathInvalid, NotFound, ReadFailed, Busy, Unavailable, TooLarge, NotUtf8]
Why read_text! produced no UTF-8 string.
PathInvalid is a path that is not plainly relative, or that meets a
symbolic link. PermissionDenied is a stub handle, which gives no
access. NotFound is no file at that path. TooLarge is a file past
read_text!'s 64 kibibyte ceiling, which is a refusal rather than a
failure: nothing went wrong and the file is there. NotUtf8 is a file
that was read and is not valid UTF-8, reported rather than delivered as
an invalid Str. Busy is the host's thirty-two file-delivery slots all
being held by byte lists an app has retained; nothing was read, and the
same call later can succeed. Unavailable is the app shutting down
while the read was parked.
ReadFailed is every other refusal, and a permission the process does
not have is one of them: the host does not distinguish it from a read
that failed for any other reason. metadata! does, so a path that may be
unreadable can be stat'd first to tell the two apart.
ReadBytesError : [PermissionDenied, PathInvalid, NotFound, ReadFailed, Busy, Unavailable, TooLarge]
Why read_bytes! produced no byte list.
The same tags as ReadTextError minus NotUtf8, since nothing about
the bytes is inspected, and with a much larger ceiling: TooLarge here
is a file past 16 mebibytes. ReadFailed covers a permission denial in
exactly the same way.
ListError : [PermissionDenied, PathInvalid, NotFound, NotADirectory, ReadFailed, Busy, Unavailable, TooLarge]
Why list! produced no directory entries.
NotADirectory is a path that is there and is a file. TooLarge is a
directory whose listing would exceed 8192 entries or one mebibyte of
encoded names, whichever binds first. The rest mean what they mean for a
read, ReadFailed included.
What one path is, how big it is, and when it last changed.
size_bytes is the file's length; for a directory it is whatever the
filesystem reports for the directory itself, which is not the size of
what is inside it. modified is wall-clock time, so it is comparable
with Time.now! and with a modified this app recorded earlier, and it
is not comparable with input.time.
MetadataError : [NotFound, AccessRefused, PathInvalid, PermissionDenied, ReadFailed, Unavailable]
Why metadata! could not describe the path.
NotFound is nothing at that path, including a path a component of
which is a file rather than a directory, and a name this filesystem
cannot represent. AccessRefused is a directory on the way to the path
this process may not look inside -- the one failure a stat can name that
a read cannot. PathInvalid and PermissionDenied mean what they mean
for a read, except that a link at the end of the path is described as
Other rather than refused. Unavailable is the app shutting down while the stat was
parked, and ReadFailed is every other refusal.
There is no Busy: a stat holds no host-owned payload, so there is no
delivery slot for it to run out of.
WriteError : [NotFound, AccessRefused, PathInvalid, PermissionDenied, NoSpace, WriteFailed, Unavailable]
Why a write did not leave the file on disk. This is Files' own
WriteError; Stdout and Stderr declare a different one under the
same name, for the different things a stream write can refuse.
NotFound means a component of the path is a file rather than a
directory, or names something that cannot be created; the missing
directories a write would otherwise trip over are created for it.
NoSpace is the filesystem being full or over quota. AccessRefused is
the operating system refusing the write, such as a read-only file or
filesystem. PathInvalid is a path that is not plainly relative, one
that meets a symbolic link, or an existing symbolic link at the path
itself. PermissionDenied is a stub handle. WriteFailed is every
other refusal the host cannot name more precisely.
OpenError : [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable]
Why a directory handle could not be opened.
PermissionDenied is a directory no declaration covers. PathInvalid
is a declared path with a .. component, which only FilesAny can
cover; only open_dir! and open_dir_read! return it. NotFound is
a read-only directory that is not there -- a writable one is created --
and NotADirectory is a path that is there and is a file.
AccessRefused is the operating system refusing to open it, and
OpenFailed is every other refusal. Unavailable is the app shutting
down while the open was parked.
PathInvalid : [PathInvalid]
Why a path given to subdir is not one a handle accepts: it is empty,
absolute, or has a backslash, a drive, a NUL, or an empty, ., or ..
component.
ReadDir
Files.ReadDir :: # (opaque)
A directory the app may read beneath, and nothing else.
Get one from io.files(), from Dir.read_only, or from subdir.
stub : ReadDir
Resource-free handle for pure tests. Every effect through it is
PermissionDenied.
The directory path names beneath this one. Pure: nothing is
opened, and a directory that is not there is found out on first use.
read_text! : ReadDir, Str => Try(Str, ReadTextError)
Read a bounded UTF-8 file into a Str: at most 64 kibibytes, and a
file past that is TooLarge rather than truncated. One that is not
valid UTF-8 is NotUtf8 rather than an invalid Str.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
read_bytes! : ReadDir, Str => Try(List(U8), ReadBytesError)
Read a bounded file as ordinary Roc bytes: at most 16 mebibytes, and
a file past that is TooLarge.
Nothing is copied, so the list owns host-backed storage; at most 32
such allocations are live at once, and a read made while all 32 are
held returns Busy without touching the disk. Retaining a sublist
retains the whole allocation, which List.release_excess_capacity
releases by copying out the part worth keeping.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
List one directory without walking its children; "" lists this
handle's own directory. Entry order is the filesystem's, unsorted.
A listing is bounded at 8192 entries and one mebibyte of encoded
names, and a directory past either is TooLarge.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
What one path is, how big it is, and when it last changed; "" is
this handle's own directory. A symbolic link is reported as Other
rather than followed.
Polling modified from a task, sleeping between stats, is how an app
hot-reloads a shader, a level, or a dataset it did not write.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
Internal bridge for platform operations that open things beneath a handle, such as an asset store or a database.
Dir
Files.Dir :: # (opaque)
A directory the app may read and write beneath, and nothing else.
Get one from io.files() or from subdir.
stub : Dir
Resource-free handle for pure tests. Every effect through it is
PermissionDenied.
read_only : Dir -> ReadDir
The same directory, without the right to write.
The directory path names beneath this one. Pure, as for ReadDir;
a missing directory is created by the first write into it.
read_text! : Dir, Str => Try(Str, ReadTextError)
As ReadDir.read_text!.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
read_bytes! : Dir, Str => Try(List(U8), ReadBytesError)
As ReadDir.read_bytes!.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
As ReadDir.list!.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
As ReadDir.metadata!.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
write_text! : Dir, Str, Str => Try({}, WriteError)
Replace a file's contents with a Str, creating it and any missing
directories on the way. The write replaces the whole file: there is
no append, and no partial write is reported as success. An existing
symbolic link at the path is refused rather than written through.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
write_bytes! : Dir, Str, List(U8) => Try({}, WriteError)
Replace a file's contents with ordinary Roc bytes, exactly as
write_text! does with a string.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
Internal bridge for platform operations that open things beneath a handle, such as a database.
Designated
Files.Designated :: # (opaque)
One file or directory the user chose for the app: dropped on its window, or named as an argument when it was launched. The user's choice counts as permission, so none is declared, and the handle gives access to exactly that item and nothing beside it.
Read it as a file with read_text!, read_bytes!, and metadata!, or,
when it is a directory, open what is inside it with dir.
stub : Designated
Resource-free handle for pure tests. Every effect through it is
PermissionDenied.
The absolute path that was designated, for showing to the user.
read_text! : Designated => Try(Str, ReadTextError)
Read the designated file into a Str, as ReadDir.read_text! does.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
read_bytes! : Designated => Try(List(U8), ReadBytesError)
Read the designated file as bytes, as ReadDir.read_bytes! does.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
What the designated item is, how big it is, and when it last changed.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
dir : Designated -> ReadDir
The designated item as a directory, when it is one. Pure; an item that is not a directory is found out on first use.
Access
Files.Access :: # (opaque)
Opaque filesystem authority supplied by App.Io: where directory handles come from.
for_host : Authority -> Access
Private platform construction; no application can manufacture the argument.
app_data! : Access => Try(Dir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
The app's private data directory, created on first use: saves,
databases, anything the app keeps for itself. Needs
App.Config.with_app_id; without one the app stops, naming it.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
app_config! : Access => Try(Dir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
The app's private configuration directory, created on first use.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
app_cache! : Access => Try(Dir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
The app's private cache directory, created on first use: files it can recreate if the user clears it.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
beside_executable! : Access => Try(ReadDir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
The directory the executable is in, read-only: the app's own bundle.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
working_directory! : Access => Try(Dir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
The launch directory, writable. Needs WorkingDirectory(ReadWrite).
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
working_directory_read! : Access => Try(ReadDir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
The launch directory, read-only. Needs WorkingDirectory in either
mode.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
open_dir! : Access, Str => Try(Dir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
A declared directory, writable. A directory that is not there is
created, with any missing parents, so the first run of an app finds
an empty directory rather than NotFound.
An absolute path names the directory directly; a relative one is opened beneath the working directory. Whether a declaration covers the path is decided from its text, not by asking the filesystem:
Directory(dir, ReadWrite)coversdirand every path beneath it written the same way: an absolute declaration covers absolute paths, a relative one covers relative paths.WorkingDirectory(ReadWrite)covers every relative path.FilesAny(ReadWrite)covers every path.
A path no declaration covers is PermissionDenied. A path with a
.. component is PathInvalid unless FilesAny covers it, because
its text alone does not say where it leads. The declared directory is
taken as named, links and all; past it, the path is opened one
component at a time, and a symbolic link on the way is PathInvalid,
so a link inside a declared directory cannot lead the handle outside
it. Under FilesAny, links are followed. Links beneath the handle
never are.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
accept_drop! : Access, Str => Try(Designated, [PermissionDenied])
Accept a file or directory the user dropped on the window, from the
update! whose input.dropped delivered it.
update! = |model, input, io| {
for drop in input.dropped {
match io.files().accept_drop!(drop.path) {
Ok(item) => Task.spawn!(input, || Opened(item.path(), item.read_bytes!()))
Err(PermissionDenied) => {}
}
}
Ok(model)
}
Only a path the host delivered this cycle is accepted; a string made
up, or kept from an earlier cycle, is PermissionDenied. Nothing is
opened, so it is legal wherever a drop can be seen.
Legal in init!, update!, and tasks; refused in render!.
Accept a file or directory named as an application argument, such
as my-tool data.csv. The operator naming it is the grant.
Only a string byte-identical to one of the app's arguments (what
io.args!() returns in init!) is accepted, and the item is
read-only. Anything else is PermissionDenied: the operator did not
name it. A relative argument names an item beneath the directory the
app was launched in, fixed when it is accepted. Legal in init!,
update!, and tasks; refused in render!.
open_dir_read! : Access, Str => Try(ReadDir, [PermissionDenied, PathInvalid, NotFound, NotADirectory, AccessRefused, OpenFailed, Unavailable])
A declared directory, read-only, covered by a declaration in either
mode. Coverage is decided as for open_dir!, but nothing is created:
a directory that is not there is NotFound.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.