Files and directories
The io module reads and writes files, lists and creates directories, and
inspects and changes what the file system records about them: permission
bits, timestamps and symbolic links. Every call that touches the file
system returns an IoResult, whose error says what went wrong portably
(is_not_found(), is_permission_denied(), is_already_exists(), …;
see Error handling with Result).
Pathis a file-system path. Joining,parent(),file_name(),extension()andnormalize()work on its text alone;exists(),metadata(),canonicalize()and theset_*methods below ask the file system.Fileis an open file.File.openreads,File.createwrites a new or emptied file, andOpenOptionsdescribes anything else (append, create only if missing, …).read_to_string,write_text,read_bytes,write_bytes,copy,renameandremovework on a whole file by path.Directorycreates and removes directories (create,create_all,remove,remove_all) and lists one, all at once withentries()or one entry at a time withfor.Metadatais a snapshot of a path’s kind, size, permission bits and timestamps, fromPath.metadata()(which follows symlinks),Path.symlink_metadata()(which does not) orFile.metadata().
# Write, read, list and remove files with io.
import io;
main(): int {
var folder = io.Directory.temp().join("pepper-files-basics-example");
io.Directory.remove_all(folder);
io.Directory.create_all(folder.join("notes")).value();
# Whole files at once.
var path = folder.join("notes").join("today.txt");
io.File.write_text(path, "buy milk\n").value();
assert(io.File.read_to_string(path).value() == "buy milk\n", "read back");
# An open File, with OpenOptions for anything but read or replace.
var log = io.OpenOptions().append(true).create(true).open(folder.join("log.txt")).value();
log.write_all("started\n").value();
log.close().value();
# Metadata, and a directory's entries.
var m = path.metadata().value();
assert(m.is_file && m.size == 9, "a 9-byte file");
var names = Array<String>();
for (var entry in io.Directory.open(folder).value()) {
names.append(entry.value().name);
}
assert(names.count == 2, "log.txt and notes");
io.Directory.remove_all(folder).value();
return 0;
}
Permissions
A file’s permission bits say who may read, write and run it. They are the
platform’s own bits, as Metadata.permissions reports them:
- On Linux and macOS, the POSIX
mode_tbits: read, write and execute for the owner, the group and everyone else, plus setuid, setgid and sticky. Pepper has no octal literals, sorwxr-xr-x(octal 755) is written 493, and a comment gives the octal. On a directory, read lets one list it, write lets one add and remove entries, and execute lets one reach anything inside it. - On Windows, the
FILE_ATTRIBUTE_*word. OnlyFILE_ATTRIBUTE_READONLYis a permission; whether a file runs depends on its extension. - WASI has no permission bits.
Three ways to change them:
set_permissions(mode)on aPathor an openFilesets the raw bits (chmodandfchmod). On Windows it applies only the read-only bit ofmode, and on WASI it returns an error.set_read_only(bool)is the portable read-only switch. On POSIX, read-only clears every write bit and writable sets only the owner’s, so this never opens a file to others. On Windows it sets or clearsFILE_ATTRIBUTE_READONLY.set_executable(bool)adds an execute bit wherever there is a read bit, aschmod +xdoes, or clears them all. It does nothing on Windows and WASI.
A file or directory can also be created with its bits, so that it never
exists with the default ones even for a moment: OpenOptions.mode(m) for a
new file, and Directory.create(path, mode = m) or create_all(path, mode = m) for a directory. The process umask still applies, as it does to open
and mkdir. These are ignored on Windows and WASI.
Timestamps
Metadata.modified and Metadata.accessed are nanoseconds since
1970-01-01 UTC. set_modified(time) and set_accessed(time) on a Path
or an open File set one and leave the other (utimensat and futimens;
SetFileTime on Windows, which keeps 100 ns). Writing to a file sets its
modification time, and adding or removing an entry sets a directory’s, so
set a time after the contents are final.
# Permission bits and timestamps: make a script executable, protect a file,
# and copy a modification time.
import io;
main(): int {
var folder = io.Directory.temp().join("pepper-files-permissions-example");
io.Directory.remove_all(folder);
# 448 is octal 700: only the owner may list or enter it (POSIX only).
io.Directory.create(folder, mode = 448).value();
# Created with mode 493 (octal 755, rwxr-xr-x) less the umask, so the
# script is never readable by others without being executable.
var script = folder.join("run.sh");
var file = io.OpenOptions().write(true).create_new(true).mode(493).open(script).value();
file.write_all("#!/bin/sh\necho hello\n").value();
file.close().value();
# The portable way: add an execute bit where there is a read bit.
var notes = folder.join("notes.sh");
io.File.write_text(notes, "#!/bin/sh\n").value();
notes.set_executable(true).value();
[Target(OS(Linux) || OS(Mac))] {
assert((notes.metadata().value().permissions & 64) != 0, "the owner may run it");
}
# Read-only: no write bits on POSIX, FILE_ATTRIBUTE_READONLY on Windows.
[Target(!OS(Wasi))] {
notes.set_read_only(true).value();
assert(notes.metadata().value().read_only, "read-only");
notes.set_read_only(false).value();
}
# Give the script the same modification time as the notes.
var modified = notes.metadata().value().modified;
script.set_modified(modified).value();
assert(script.metadata().value().modified == modified, "same time");
io.Directory.remove_all(folder).value();
return 0;
}
On Windows, an open File must have been opened for writing to change its
read-only bit or its times.
Symbolic links
File.symlink(target, link) and Directory.symlink(target, link) create
a symbolic link at link pointing to target. POSIX makes one kind of
link either way; Windows makes a file link and a directory link
differently, and a file link to a directory does not open, so use the one
that matches the target. The target is stored as given and need not exist;
a relative target is resolved from the directory holding the link.
Path.read_link() returns the target a link stores, without following it.
Path.symlink_metadata() describes the link itself, and metadata() what
it points to. Directory.remove_all removes links and never follows them.
# Symbolic links: make one to a file and one to a directory, and read them.
import io;
main(): int {
var folder = io.Directory.temp().join("pepper-files-symlinks-example");
io.Directory.remove_all(folder);
io.Directory.create_all(folder.join("releases").join("1.2")).value();
io.File.write_text(folder.join("app-1.2.conf"), "port = 80\n").value();
# Windows needs Developer Mode or an administrator to make symlinks.
[Target(!OS(Windows))] {
# A relative target is resolved from the folder holding the link.
var config = folder.join("app.conf");
io.File.symlink(io.Path("app-1.2.conf"), config).value();
assert(io.File.read_to_string(config).value() == "port = 80\n", "read through the link");
assert(config.read_link().value() == io.Path("app-1.2.conf"), "the stored target");
assert(config.symlink_metadata().value().is_symlink, "the link itself");
var current = folder.join("current");
io.Directory.symlink(io.Path("releases").join("1.2"), current).value();
assert(current.is_directory(), "a directory through the link");
}
# remove_all removes links, never what they point to.
io.Directory.remove_all(folder).value();
return 0;
}
Making a symlink on Windows needs Developer Mode or an administrator;
without either it fails with is_permission_denied().
Moving a file to another thread
A File is not thread-safe, so a thread cannot capture one. To hand an
open file to another thread, such as the pipe from a child process’s
output to a thread that reads it while this one writes to the child,
file.into_transfer() moves its handle into an io.FileTransfer, a
thread-safe token that a shared function can capture or a Channel can
carry, and io.File.adopt(transfer) takes it out again on the other
thread. The File it came from is closed afterwards, only the first adopt
gets the handle, and a token no one adopts closes the file when it goes.
A stream moves the same way, as a SocketTransfer (see
Moving a stream to another worker).
# Hand an open File to another thread with into_transfer and adopt.
import async;
import io;
main(): int {
var path = io.Directory.temp().join("pepper-files-transfer-example.txt");
io.File.write_text(path, "read on another thread").value();
var file = io.File.open(path).value();
# A File is not thread-safe; its transfer token is.
var transfer = file.into_transfer();
var size = async.Atomic<ulong>(0);
var reader = async.Thread.spawn(() => {
var mine = io.File.adopt(transfer).value();
size.store(mine.read_all().value().count);
});
reader.join();
assert(size.load() == 22, "the other thread read the file");
assert(!file.is_open(), "the File it came from is closed");
io.File.remove(path);
return 0;
}
Archives
archive.TarReader.extract uses all of the above: it restores each file’s
mode and modification time and makes the archive’s symlinks, as long as
they stay inside the directory it extracts into. See
Compression and archives.