# "Filesystem" _Path: en/lua/storage/filesystem_ > "Read, write, and manage files within sandboxed filesystem volumes." ## Table of Contents - Filesystem ## Content # Filesystem The `fs` module reads, writes, and manages files within configured filesystem volumes. This page is an API reference. Its snippets assume a configured volume and permission to acquire it. Each block is an isolated operation or partial recipe; application values and callbacks such as `config`, `message`, `process`, and `report_cleanup_error` must already exist. `report_cleanup_error(err)` records a close failure without replacing an operation error that already occurred. For filesystem configuration, see [Filesystem](system/filesystem.md). ## Loading ```lua local fs = require("fs") ``` ## Acquiring a Volume Acquire a filesystem volume by registry ID: ```lua local vol, err = fs.get("app:storage") if err then return nil, err end local content, read_err = vol:readfile("/config.json") if read_err then return nil, read_err end return content ``` | Parameter | Type | Description | |-----------|------|-------------| | `name` | string | Volume registry ID | **Returns:** `FS, error` Volumes do not require explicit release. The system manages them, and a volume becomes unavailable when its filesystem is detached from the registry. ## Reading Files Read an entire file: ```lua local json = require("json") local vol, get_err = fs.get("app:config") if get_err then return nil, get_err end local data, err = vol:readfile("/settings.json") if err then return nil, err end local config, decode_err = json.decode(data) if decode_err then return nil, decode_err end return config ``` Use `open()` to stream a large file: ```lua local errors = require("errors") local file, err = vol:open("/data/large.csv", "r") if err then return nil, err end while true do local chunk, err = file:read(65536) if err then if err:kind() == errors.NOT_FOUND then break -- EOF end local _, close_err = file:close() if close_err then report_cleanup_error(close_err) end return nil, err end process(chunk) end local _, close_err = file:close() if close_err then return nil, close_err end ``` ## Writing Files Write a string or reader-backed stream to a file: ```lua local json = require("json") local vol, get_err = fs.get("app:data") if get_err then return nil, get_err end -- Overwrite (default) local encoded, encode_err = json.encode(config) if encode_err then return nil, encode_err end local _, write_err = vol:writefile("/config.json", encoded) if write_err then return nil, write_err end -- Append local _, append_err = vol:writefile("/logs/app.log", message .. "\n", "a") if append_err then return nil, append_err end -- Exclusive write (fails if exists) local ok, err = vol:writefile("/lock.pid", tostring(pid), "wx") if err then return nil, err end -- Copy from an open file or another reader-backed value local source, err = vol:open("/incoming/report.csv", "r") if err then return nil, err end local copied, err = vol:writefile("/archive/report.csv", source) local _, close_err = source:close() if err then if close_err then report_cleanup_error(close_err) end return nil, err end if close_err then return nil, close_err end return copied ``` | Mode | Description | |------|-------------| | `"w"` | Overwrite (default) | | `"a"` | Append | | `"wx"` | Exclusive write (fails if file exists) | Use a file handle for streaming writes: ```lua local file, open_err = vol:open("/output/report.txt", "w") if open_err then return nil, open_err end local _, header_err = file:write("Header\n") if header_err then local _, close_err = file:close() if close_err then report_cleanup_error(close_err) end return nil, header_err end local _, data_err = file:write("Data: " .. value .. "\n") if data_err then local _, close_err = file:close() if close_err then report_cleanup_error(close_err) end return nil, data_err end local _, sync_err = file:sync() if sync_err then local _, close_err = file:close() if close_err then report_cleanup_error(close_err) end return nil, sync_err end local _, close_err = file:close() if close_err then return nil, close_err end ``` ## Checking Paths ```lua local vol, get_err = fs.get("app:data") if get_err then return nil, get_err end -- Check existence local exists, exists_err = vol:exists("/cache/results.json") if exists_err then return nil, exists_err end if exists then return vol:readfile("/cache/results.json") end -- Check if directory local is_dir, isdir_err = vol:isdir(path) if isdir_err then return nil, isdir_err end if is_dir then process_directory(path) end -- Get file info local info, stat_err = vol:stat("/documents/report.pdf") if stat_err then return nil, stat_err end print(info.size, info.modified, info.type) ``` **Stat fields:** `name`, `size`, `mode`, `modified`, `is_dir`, `type` ## Directory Operations ```lua local vol, get_err = fs.get("app:data") if get_err then return nil, get_err end -- Create directory local _, mkdir_err = vol:mkdir("/uploads/" .. user_id) if mkdir_err then return nil, mkdir_err end -- List directory contents local iter, state = vol:readdir("/documents") if not iter then return nil, state end for entry in iter, state do print(entry.name, entry.type) end -- Remove file or empty directory local removed, remove_err = vol:remove("/temp/file.txt") if remove_err then return nil, remove_err end return removed ``` Entry fields: `name`, `type` ("file" or "directory") `mkdir` creates one directory and does not create missing parents. `remove` accepts files and empty directories only. ## File Handle Methods When using `vol:open()` for streaming: | Method | Description | |--------|-------------| | `read(size?)` | Read bytes (default: 4096) | | `write(data)` | Write string data | | `seek(whence, offset)` | Set position ("set", "cur", "end") | | `stat()` | Get file info (same fields as `vol:stat`) | | `sync()` | Flush to storage | | `close()` | Release file handle | | `scanner(split?)` | Create line/word scanner | Call `close()` after finishing with a file handle. ## Scanner Use a scanner for line-by-line processing: ```lua local file, err = vol:open("/data/users.csv", "r") if err then return nil, err end local scanner, err = file:scanner("lines") if err then local _, close_err = file:close() if close_err then report_cleanup_error(close_err) end return nil, err end scanner:scan() -- skip header while scanner:scan() do local line = scanner:text() process(line) end local scan_err = scanner:err() if scan_err then local _, close_err = file:close() if close_err then report_cleanup_error(close_err) end return nil, scan_err end local _, close_err = file:close() if close_err then return nil, close_err end ``` Split modes: `"lines"` (default), `"words"`, `"bytes"`, `"runes"` `scanner:scan()` returns only a boolean. When it returns `false`, call `scanner:err()` to distinguish clean EOF from a tokenization or underlying read failure. `scanner:err()` returns a structured `INTERNAL` error or `nil`; unlike a stream scanner, a file scanner has no separate scan-dispatch error return. ## Constants ```lua fs.type.FILE -- "file" fs.type.DIR -- "directory" fs.seek.SET -- from start fs.seek.CUR -- from current fs.seek.END -- from end ``` ## FS Methods | Method | Returns | Description | |--------|---------|-------------| | `readfile(path)` / `read_file(path)` | `string, error` | Read entire file | | `writefile(path, data, mode?)` / `write_file(path, data, mode?)` | `boolean, error` | Write a string or reader-backed value | | `exists(path)` | `boolean, error` | Check if path exists | | `stat(path)` | `table, error` | Get file info | | `isdir(path)` | `boolean, error` | Check if directory | | `mkdir(path)` | `boolean, error` | Create directory | | `remove(path)` | `boolean, error` | Remove file/empty dir | | `readdir(path)` | `iterator, state` | List directory (use in generic `for` loop) | | `open(path, mode)` | `File, error` | Open file handle | | `chdir(path)` | `boolean, error` | Change working dir | | `pwd()` | `string, error` | Get working dir | ## Permissions Security policy evaluation applies when a volume is acquired. | Action | Resource | Description | |--------|----------|-------------| | `fs.get` | Volume ID | Acquire filesystem volume | ## Errors | Condition | Kind | Retryable | |-----------|------|-----------| | Empty path | `errors.INVALID` | unspecified | | Path contains a null byte | `errors.INVALID` | no | | Invalid mode | `errors.INVALID` | unspecified | | `scanner()` called on a closed file | `errors.INVALID` | unspecified | | Read, write, seek, stat, or sync called on a closed file | `errors.INTERNAL` | no | | `close()` called on an already closed file | succeeds | not applicable | | File-handle read reached EOF | `errors.NOT_FOUND` | unspecified | | Path not found | `errors.NOT_FOUND` | preserved from the underlying error when available | | Path already exists | `errors.ALREADY_EXISTS` | unspecified | | Permission denied | `errors.PERMISSION_DENIED` | no | | File scanner tokenization or read failed | `errors.INTERNAL` | preserved from the underlying error when available | `unspecified` means `err:retryable()` returns `nil`; it is not equivalent to `false`. See [Error Handling](lua/core/errors.md) for working with errors. ## Navigation Previous: "Key-Value Store" (lua/storage/store) Next: "Cloud Storage" (lua/storage/cloud)