Filesystem API
Reading config files at startup, writing logs, processing data pipelines, managing temp directories — filesystem operations are everywhere. The fs module gives you two ways to work with files: synchronous (File) for simple scripts and startup logic, and async (AsyncFile) for high-concurrency servers where blocking the event loop is not an option.
Most of the time you’ll reach for the convenience functions — readFile, writeFile, copy — which handle opening, reading/writing, and closing in a single call. When you need fine-grained control (seeking, partial reads, metadata inspection), drop down to File or AsyncFile handles.
At a Glance
Section titled “At a Glance”const fs = @import("volt").fs;
// One-liner: read an entire file into memoryconst data = try fs.readFile(allocator, "config.json");defer allocator.free(data);
// One-liner: write a string to a file (creates or truncates)try fs.writeFile("output.txt", "Hello, world!");
// File handle for more controlvar file = try fs.File.open("data.bin");defer file.close();
var buf: [4096]u8 = undefined;const n = try file.read(&buf);// Directory operationstry fs.createDirAll("logs/2026/02");
var iter = try fs.readDir("logs/2026/02");defer iter.close();
while (try iter.next()) |entry| { std.debug.print("{s} ({} bytes)\n", .{ entry.name, entry.size });}// Async file I/O (requires Io handle -- uses io_uring on Linux, blocking pool elsewhere)var async_file = try fs.AsyncFile.open(io, "large_dataset.bin");defer async_file.close();
const chunk = try async_file.read(&buf);Module Import
Section titled “Module Import”const fs = @import("volt").fs;All types and functions are accessed through the fs namespace: fs.File, fs.readFile, fs.createDir, etc.
Convenience Functions
Section titled “Convenience Functions”These handle the full open-read/write-close lifecycle in one call. Prefer these for simple operations.
fs.readFile
Section titled “fs.readFile”pub fn readFile(allocator: Allocator, path: []const u8) ![]u8Read the entire contents of a file into a newly allocated buffer. Caller owns the returned slice.
const config = try fs.readFile(allocator, "settings.json");defer allocator.free(config);
const parsed = try std.json.parseFromSlice(Config, allocator, config, .{});fs.readFileString
Section titled “fs.readFileString”pub fn readFileString(allocator: Allocator, path: []const u8) ![]u8Like readFile, but validates that the contents are valid UTF-8. Returns error.InvalidUtf8 if validation fails.
const template = try fs.readFileString(allocator, "email_template.txt");defer allocator.free(template);fs.writeFile
Section titled “fs.writeFile”pub fn writeFile(path: []const u8, data: []const u8) !voidWrite data to a file, creating it if it doesn’t exist, truncating it if it does.
const output = try std.json.stringifyAlloc(allocator, result, .{});defer allocator.free(output);try fs.writeFile("results.json", output);fs.appendFile
Section titled “fs.appendFile”pub fn appendFile(path: []const u8, data: []const u8) !voidAppend data to a file, creating it if it doesn’t exist. Useful for logs and audit trails.
const timestamp = std.time.timestamp();var buf: [128]u8 = undefined;const line = std.fmt.bufPrint(&buf, "[{d}] Request processed\n", .{timestamp}) catch return;try fs.appendFile("app.log", line);fs.copy
Section titled “fs.copy”pub fn copy(src: []const u8, dst: []const u8) !voidCopy a file from src to dst. Creates the destination if it doesn’t exist, truncates if it does.
// Backup before overwritingtry fs.copy("database.db", "database.db.bak");fs.rename
Section titled “fs.rename”pub fn rename(old: []const u8, new: []const u8) !voidRename or move a file or directory. Atomic on most filesystems when source and destination are on the same mount.
// Atomic write pattern: write to temp, then rename into placetry fs.writeFile("config.json.tmp", new_config);try fs.rename("config.json.tmp", "config.json");fs.exists
Section titled “fs.exists”pub fn exists(path: []const u8) !boolCheck if a path exists. Returns an error if the check itself fails (e.g., permission denied on parent directory).
if (try fs.exists("config.toml")) { const config = try fs.readFile(allocator, "config.toml"); defer allocator.free(config); // ...}fs.tryExists
Section titled “fs.tryExists”pub fn tryExists(path: []const u8) boolCheck if a path exists, returning false on any error. Convenient when you don’t care about the failure reason.
const config_path = if (fs.tryExists("config.local.toml")) "config.local.toml"else "config.toml";fs.File
Section titled “fs.File”A handle to an open file. Provides read, write, seek, and metadata operations. Works without a runtime — no async machinery needed.
Construction
Section titled “Construction”| Method | Description |
|---|---|
File.open(path) | Open for reading |
File.create(path) | Create or truncate for writing |
File.createNew(path) | Create new file, fail if exists |
File.openWithOptions(path, opts) | Open with custom OpenOptions |
// Read an existing filevar input = try fs.File.open("data.csv");defer input.close();
// Create (or overwrite) an output filevar output = try fs.File.create("report.csv");defer output.close();
// Fail if the file already exists (safe for lock files, temp files)var lock = try fs.File.createNew("/tmp/myapp.lock");defer lock.close();Reading
Section titled “Reading”| Method | Signature | Description |
|---|---|---|
read | fn read(buf: []u8) !usize | Read up to buf.len bytes, returns bytes read |
readAll | fn readAll(buf: []u8) !usize | Fill the buffer completely (retries on partial reads) |
pread | fn pread(buf: []u8, offset: u64) !usize | Read at a specific offset without seeking |
readToEnd | fn readToEnd(allocator: Allocator) ![]u8 | Read entire remaining contents |
readVectored | fn readVectored(iovecs: []posix.iovec) !usize | Scatter read into multiple buffers |
var file = try fs.File.open("binary.dat");defer file.close();
// Read a fixed-size headervar header: [64]u8 = undefined;const n = try file.readAll(&header);if (n < 64) return error.UnexpectedEof;
// Read the rest into a dynamic bufferconst body = try file.readToEnd(allocator);defer allocator.free(body);Writing
Section titled “Writing”| Method | Signature | Description |
|---|---|---|
write | fn write(data: []const u8) !usize | Write up to data.len bytes, returns bytes written |
writeAll | fn writeAll(data: []const u8) !void | Write all bytes (retries on partial writes) |
pwrite | fn pwrite(data: []const u8, offset: u64) !usize | Write at a specific offset without seeking |
writeVectored | fn writeVectored(iovecs: []const posix.iovec_const) !usize | Gather write from multiple buffers |
var file = try fs.File.create("output.bin");defer file.close();
try file.writeAll(&header_bytes);try file.writeAll(payload);try file.syncAll(); // Flush to diskSeeking
Section titled “Seeking”| Method | Signature | Description |
|---|---|---|
seekTo | fn seekTo(offset: u64) !void | Seek to absolute position |
seekBy | fn seekBy(offset: i64) !void | Seek relative to current position |
rewind | fn rewind() !void | Seek to the beginning |
getPos | fn getPos() !u64 | Get current position |
getLen | fn getLen() !u64 | Get file length |
var file = try fs.File.open("index.bin");defer file.close();
// Jump to a record by indexconst record_size: u64 = 256;try file.seekTo(record_idx * record_size);
var record: [256]u8 = undefined;_ = try file.readAll(&record);Sync and Metadata
Section titled “Sync and Metadata”| Method | Signature | Description |
|---|---|---|
syncAll | fn syncAll() !void | Flush data + metadata to disk |
syncData | fn syncData() !void | Flush data only (faster, skips metadata) |
metadata | fn metadata() !Metadata | Get file metadata |
setLen | fn setLen(len: u64) !void | Truncate or extend the file |
setPermissions | fn setPermissions(perms: Permissions) !void | Set file permissions |
var file = try fs.File.create("important.dat");defer file.close();
try file.writeAll(critical_data);try file.syncAll(); // Ensure data survives a crashReader/Writer Interfaces
Section titled “Reader/Writer Interfaces”Files support standard io.Reader and io.Writer interfaces for composition with buffered I/O, compression, serialization, etc.
var file = try fs.File.open("data.txt");defer file.close();
var reader = file.reader();// Use with any code that accepts an io.Readerpub fn close(self: *File) voidClose the file handle. Always use defer file.close() immediately after opening.
AsyncFile
Section titled “AsyncFile”fs.AsyncFile
Section titled “fs.AsyncFile”An async file handle that integrates with the Volt runtime. On Linux with io_uring, file operations are truly asynchronous — submitted directly to the kernel’s completion ring. On other platforms (macOS, BSD, Windows), operations run on the blocking thread pool so they don’t stall the event loop.
Construction
Section titled “Construction”| Method | Description |
|---|---|
AsyncFile.open(io, path) | Open for reading |
AsyncFile.create(io, path) | Create or truncate for writing |
AsyncFile.createNew(io, path) | Create new file, fail if exists |
var file = try fs.AsyncFile.open(io, "large_dataset.csv");defer file.close();
var buf: [8192]u8 = undefined;const n = try file.read(&buf);Async Convenience Functions
Section titled “Async Convenience Functions”These are the async equivalents of the top-level convenience functions. They require an Io handle.
// Read entire file without blocking the event loopconst data = try fs.readFileAsync(io, allocator, "big_file.json");defer allocator.free(data);
// Write without blockingtry fs.writeFileAsync(io, "output.dat", processed_data);
// Append without blockingtry fs.appendFileAsync(io, "events.log", log_line);| Function | Signature | Description |
|---|---|---|
readFileAsync | fn(io, allocator, path) ![]u8 | Read entire file asynchronously |
writeFileAsync | fn(io, path, data) !void | Write file asynchronously |
appendFileAsync | fn(io, path, data) !void | Append to file asynchronously |
OpenOptions
Section titled “OpenOptions”fs.OpenOptions
Section titled “fs.OpenOptions”Builder pattern for fine-grained control over how files are opened. Every setter returns a new OpenOptions, so calls can be chained.
| Field | Default | Description |
|---|---|---|
read | false | Open for reading |
write | false | Open for writing |
append | false | Writes go to end of file |
truncate | false | Truncate file to 0 bytes on open |
create | false | Create file if it doesn’t exist |
create_new | false | Create file, fail if it already exists |
mode | 0o666 | Unix permissions for new files (modified by umask) |
custom_flags | 0 | Platform-specific open flags |
// Read + write (update in place)var file = try fs.OpenOptions.new() .setRead(true) .setWrite(true) .open("database.db");defer file.close();
// Append-only log filevar log = try fs.OpenOptions.new() .setWrite(true) .setCreate(true) .setAppend(true) .open("audit.log");defer log.close();
// Create with restricted permissions (Unix)var secret = try fs.OpenOptions.new() .setWrite(true) .setCreateNew(true) .setMode(0o600) // owner read/write only .open("/etc/myapp/secret.key");defer secret.close();Directory Operations
Section titled “Directory Operations”fs.createDir
Section titled “fs.createDir”pub fn createDir(path: []const u8) !voidCreate a single directory. Fails if the parent doesn’t exist or the directory already exists.
fs.createDirAll
Section titled “fs.createDirAll”pub fn createDirAll(path: []const u8) !voidCreate a directory and all missing parent directories. Succeeds silently if the directory already exists.
// Ensure the full path exists before writingtry fs.createDirAll("data/exports/2026/02");try fs.writeFile("data/exports/2026/02/report.csv", csv_data);fs.createDirMode
Section titled “fs.createDirMode”pub fn createDirMode(path: []const u8, mode: u32) !voidCreate a directory with specific Unix permissions.
try fs.createDirMode("/var/run/myapp", 0o755);fs.removeDir
Section titled “fs.removeDir”pub fn removeDir(path: []const u8) !voidRemove an empty directory. Fails if the directory is not empty.
fs.removeDirAll
Section titled “fs.removeDirAll”pub fn removeDirAll(allocator: Allocator, path: []const u8) !voidRemove a directory and all its contents recursively. Use with caution.
// Clean up temp directory after processingtry fs.removeDirAll(allocator, "/tmp/myapp-work");fs.removeFile
Section titled “fs.removeFile”pub fn removeFile(path: []const u8) !voidRemove a single file.
fs.readDir
Section titled “fs.readDir”pub fn readDir(path: []const u8) !ReadDirOpen a directory for iteration. Returns a ReadDir iterator.
var iter = try fs.readDir("uploads/");defer iter.close();
while (try iter.next()) |entry| { if (entry.isFile()) { std.debug.print("File: {s}\n", .{entry.name}); }}fs.ReadDir
Section titled “fs.ReadDir”Iterator over directory entries. Call next() to get the next DirEntry, or null when done.
| Method | Description |
|---|---|
next() !?DirEntry | Get next entry, or null at end |
close() | Close the directory handle |
fs.DirEntry
Section titled “fs.DirEntry”A single entry from a directory listing.
| Field/Method | Description |
|---|---|
name | Entry name (filename only, not full path) |
isFile() | Returns true if this is a regular file |
isDir() | Returns true if this is a directory |
isSymlink() | Returns true if this is a symbolic link |
fs.DirBuilder
Section titled “fs.DirBuilder”Builder for creating directories with options.
var builder = fs.DirBuilder.new();builder.setRecursive(true);builder.setMode(0o750);try builder.create("path/to/dir");Metadata
Section titled “Metadata”fs.metadata
Section titled “fs.metadata”pub fn metadata(path: []const u8) !MetadataGet metadata for a file or directory. Follows symlinks.
const meta = try fs.metadata("data.db");std.debug.print("Size: {} bytes\n", .{meta.size()});std.debug.print("Type: {s}\n", .{if (meta.isFile()) "file" else "directory"});fs.symlinkMetadata
Section titled “fs.symlinkMetadata”pub fn symlinkMetadata(path: []const u8) !MetadataGet metadata for a path without following symlinks. If the path is a symlink, returns metadata about the symlink itself.
fs.Metadata
Section titled “fs.Metadata”| Method | Return Type | Description |
|---|---|---|
fileType() | FileType | File, directory, symlink, etc. |
isFile() | bool | Is a regular file? |
isDir() | bool | Is a directory? |
isSymlink() | bool | Is a symbolic link? |
size() | u64 | Size in bytes |
permissions() | Permissions | File permissions |
modified() | i64 | Last modified (Unix epoch seconds) |
accessed() | i64 | Last accessed (Unix epoch seconds) |
created() | ?i64 | Creation time (platform-dependent, may be null) |
modifiedTime() | SystemTime | Last modified as SystemTime |
accessedTime() | SystemTime | Last accessed as SystemTime |
const meta = try fs.metadata("report.pdf");
// Check ageconst mtime = meta.modifiedTime();if (mtime.elapsed().asSecs() > 86400) { std.debug.print("File is more than a day old\n", .{});}
// Check sizeif (meta.size() > 100 * 1024 * 1024) { std.debug.print("Warning: file is over 100 MB\n", .{});}fs.Permissions
Section titled “fs.Permissions”File permission bits. Provides methods for querying Unix permission modes.
const meta = try fs.metadata("script.sh");const perms = meta.permissions();
if (!perms.isExecutable()) { try fs.setPermissions("script.sh", perms.withExecutable(true));}fs.setPermissions
Section titled “fs.setPermissions”pub fn setPermissions(path: []const u8, perms: Permissions) !voidSet permissions on a file or directory.
fs.FileType
Section titled “fs.FileType”Enum representing the type of a filesystem entry:
| Variant | Description |
|---|---|
.file | Regular file |
.directory | Directory |
.sym_link | Symbolic link |
.block_device | Block device |
.character_device | Character device |
.named_pipe | Named pipe (FIFO) |
.unix_domain_socket | Unix domain socket |
.unknown | Unknown type |
fs.SystemTime
Section titled “fs.SystemTime”A point in time from the filesystem. Provides ergonomic methods for working with timestamps.
| Method | Description |
|---|---|
elapsed() | Duration since this time (returns Duration) |
durationSince(other) | Duration between two system times |
Link Operations
Section titled “Link Operations”fs.hardLink
Section titled “fs.hardLink”pub fn hardLink(src: []const u8, dst: []const u8) !voidCreate a hard link. Both paths will point to the same underlying data.
fs.symlink
Section titled “fs.symlink”pub fn symlink(target: []const u8, link_path: []const u8) !voidCreate a symbolic link at link_path pointing to target.
try fs.symlink("config.production.toml", "config.toml");fs.readLink
Section titled “fs.readLink”pub fn readLink(allocator: Allocator, path: []const u8) ![]u8Read the target of a symbolic link. Caller owns the returned path.
const target = try fs.readLink(allocator, "config.toml");defer allocator.free(target);std.debug.print("config.toml -> {s}\n", .{target});Path Operations
Section titled “Path Operations”fs.canonicalize
Section titled “fs.canonicalize”pub fn canonicalize(allocator: Allocator, path: []const u8) ![]u8Resolve a path to its canonical, absolute form. Resolves all symlinks and removes . and .. components.
const abs = try fs.canonicalize(allocator, "../data/input.csv");defer allocator.free(abs);// e.g., "/home/user/project/data/input.csv"Common Patterns
Section titled “Common Patterns”Atomic File Write
Section titled “Atomic File Write”Write to a temporary file, then atomically rename it into place. This prevents readers from ever seeing a partially-written file:
const fs = @import("volt").fs;
fn atomicWrite(path: []const u8, data: []const u8) !void { const tmp_path = path ++ ".tmp"; try fs.writeFile(tmp_path, data); try fs.rename(tmp_path, path);}Config File Loading with Fallback
Section titled “Config File Loading with Fallback”const fs = @import("volt").fs;
fn loadConfig(allocator: std.mem.Allocator) ![]u8 { // Try local override first, then fall back to default const path = if (fs.tryExists("config.local.toml")) "config.local.toml" else if (fs.tryExists("config.toml")) "config.toml" else return error.NoConfigFile;
return fs.readFile(allocator, path);}Log Rotation
Section titled “Log Rotation”const fs = @import("volt").fs;
fn rotateLogIfNeeded(log_path: []const u8, max_size: u64) !void { const meta = fs.metadata(log_path) catch return; // No file = nothing to rotate if (meta.size() < max_size) return;
// Rotate: current -> .1, .1 -> .2, etc. fs.rename(log_path ++ ".1", log_path ++ ".2") catch {}; try fs.rename(log_path, log_path ++ ".1");}Directory Walk
Section titled “Directory Walk”Process all files in a directory tree:
const std = @import("std");const fs = @import("volt").fs;
fn processDirectory(allocator: std.mem.Allocator, path: []const u8) !void { var iter = try fs.readDir(path); defer iter.close();
while (try iter.next()) |entry| { if (entry.isDir()) { // Recurse into subdirectories const subpath = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ path, entry.name }); defer allocator.free(subpath); try processDirectory(allocator, subpath); } else if (entry.isFile()) { std.debug.print("Processing: {s}/{s}\n", .{ path, entry.name }); } }}High-Concurrency File Processing
Section titled “High-Concurrency File Processing”Use AsyncFile inside the runtime to process many files without blocking I/O workers:
const volt = @import("volt");const fs = volt.fs;
fn processFiles(io: volt.Io, paths: []const []const u8) void { for (paths) |path| { const f = io.@"async"(struct { fn run(io_inner: volt.Io, p: []const u8) void { // AsyncFile uses io_uring on Linux, blocking pool elsewhere const data = fs.readFileAsync(io_inner, std.heap.page_allocator, p) catch return; defer std.heap.page_allocator.free(data);
// Process the data... } }.run, .{ io, path }); f.detach(); // fire-and-forget: each file processes independently }}