README

#File System API (@moonbitlang/async/fs)

Asynchronous file system operations for MoonBit. This package provides comprehensive APIs for working with files, directories, and file metadata.

#Table of Contents

#File Operations

#Opening and Creating Files

The open function provides flexible file opening with various modes and options:

///|
#cfg(target="native")
async test "open file for reading" {
let test_file = "_build/test_open_read.txt"
@fs.write_file(test_file, b"Hello, MoonBit!", create_mode=CreateOrTruncate)
let file = @fs.open(test_file, mode=ReadOnly)
defer file.close()
let content = file.read_all().text()
@fs.remove(test_file)
inspect(content, content="Hello, MoonBit!")
}

///|
#cfg(target="native")
async test "open file for writing" {
let test_file = "_build/test_open_write.txt"
let file = @fs.open(test_file, mode=WriteOnly, create_mode=CreateOrTruncate)
defer file.close()
file.write(b"Hello, World!")
@fs.remove(test_file)
}

///|
#cfg(target="native")
async test "open with append mode" {
let test_file = "_build/test_append.txt"
// Create initial file
@fs.write_file(test_file, b"First line\n", create_mode=CreateOrTruncate)

// Append to existing file
let file = @fs.open(test_file, mode=WriteOnly, append=true)
file.write(b"Second line\n")
file.close()
let content = @fs.read_file(test_file).text()
@fs.remove(test_file)
inspect(content, content="First line\nSecond line\n")
}

When append=true, sequential writes through the @io.Writer interface always append to the current end of the file, even if another process extends the file after it is opened. Append mode does not change read behavior, and random-access writes with write_at are not supported on append-mode files.

The create function is a convenience wrapper for creating new files:

///|
#cfg(target="native")
async test "create new file" {
let test_file = "_build/test_create.txt"
let file = @fs.create(test_file)
file.write(b"New file content")
file.close()
let exists = @fs.exists(test_file)
@fs.remove(test_file)
inspect(exists, content="true")
}

#Reading Files

Read entire files or read data in chunks:

///|
#cfg(target="native")
async test "read_file - read entire file" {
let test_file = "_build/test_read_file.txt"
@fs.write_file(test_file, b"Hello, MoonBit!", create_mode=CreateOrTruncate)
let content = @fs.read_file(test_file)
@fs.remove(test_file)
inspect(content.text(), content="Hello, MoonBit!")
}

///|
#cfg(target="native")
async test "read in chunks using File" {
let test_file = "_build/test_chunk_read.txt"
@fs.write_file(test_file, b"0123456789", create_mode=CreateOrTruncate)
let file = @fs.open(test_file, mode=ReadOnly)
defer file.close()
let buf = FixedArray::make(5, b'0')
let n = file.read(buf)
@fs.remove(test_file)
inspect(n, content="5")
inspect(@utf8.decode(buf.unsafe_reinterpret_as_bytes()), content="01234")
}

///|
#cfg(target="native")
async test "read_all from file" {
let test_file = "_build/test_read_all.txt"
@fs.write_file(test_file, b"Complete content", create_mode=CreateOrTruncate)
let file = @fs.open(test_file, mode=ReadOnly)
let data = file.read_all()
file.close()
@fs.remove(test_file)
inspect(data.text(), content="Complete content")
}

///|
#cfg(target="native")
async test "read_exactly specific bytes" {
let test_file = "_build/test_read_exact.txt"
@fs.write_file(test_file, b"1234567890", create_mode=CreateOrTruncate)
let file = @fs.open(test_file, mode=ReadOnly)
let bytes = file.read_exactly(5)
file.close()
@fs.remove(test_file)
inspect(@utf8.decode(bytes), content="12345")
}

When reading through the @io.Reader interface, a File is read as a byte stream. The read stream position is independent of the write stream position.

#Writing Files

Write data to files using various methods:

///|
#cfg(target="native")
async test "write_file - write entire file" {
let test_file = "_build/test_write.txt"
@fs.write_file(test_file, b"File content", create_mode=CreateOrTruncate)
let content = @fs.read_file(test_file).text()
@fs.remove(test_file)
inspect(content, content="File content")
}

///|
#cfg(target="native")
async test "write with sync modes" {
let test_file = "_build/test_sync.txt"
// Write with data sync
@fs.write_file(
test_file,
b"Synced data",
sync=Data,
create_mode=CreateOrTruncate,
)
let content = @fs.read_file(test_file).text()
@fs.remove(test_file)
inspect(content, content="Synced data")
}

///|
#cfg(target="native")
async test "write using File methods" {
let test_file = "_build/test_file_write.txt"
let file = @fs.create(test_file)
file.write(b"Line 1\n")
file.write(b"Line 2\n")
file.close()
let content = @fs.read_file(test_file).text()
@fs.remove(test_file)
inspect(content, content="Line 1\nLine 2\n")
}

///|
#cfg(target="native")
async test "write_once for single write operation" {
let test_file = "_build/test_write_once.txt"
let file = @fs.create(test_file)
let data : Bytes = b"Single write"
let written = file.write_once(data, offset=0, len=data.length())
file.close()
@fs.remove(test_file)
inspect(written, content="12")
}

Sequential writes through the @io.Writer interface write a byte stream starting at offset 0 by default. The write stream position is independent of the read stream position. For files opened with append=true, sequential writes append to the end of the file instead.

#Random access on files

Read and write file from specified position:

///|
#cfg(target="native")
async test "read at specific position" {
let test_file = "_build/read_at_test.txt"
@fs.write_file(test_file, b"0123456789", create_mode=CreateOrTruncate)
{
let file = @fs.open(test_file, mode=ReadOnly)
defer file.close()

// read 3 bytes at position 5
json_inspect(file.read_exactly_at(3, position=5), content="567")

// use `read_at` to handle EOF robustly
let buf = FixedArray::make(10, b'\x00')
let n = file.read_at(buf, position=5)
inspect(n, content="5")
json_inspect(buf.unsafe_reinterpret_as_bytes()[:n], content="56789")
}
@fs.remove(test_file)
}

///|
#cfg(target="native")
async test "write at specific position" {
let test_file = "_build/write_at_test.txt"
{
let file = @fs.open(test_file, mode=WriteOnly, create_mode=CreateOrTruncate)
defer file.close()
file.write("abcdef")
file.write_at(b"CD", position=2)
}

// read 3 bytes at position 5
inspect(@fs.read_file(test_file).text(), content="abCDef")
@fs.remove(test_file)
}

///|
#cfg(target="native")
async test "size - get file size" {
let test_file = "_build/test_size.txt"
@fs.write_file(test_file, b"Hello", create_mode=CreateOrTruncate)
let file = @fs.open(test_file, mode=ReadOnly)
let size = file.size()
file.close()
@fs.remove(test_file)
inspect(size, content="5")
}

Some important notes when using read_at and write_at:

  • only seekable files (i.e. regular files or block devices) support read_at and write_at. Calling read_at and write_at on unsupported file types result in error
  • read_at and write_at does not modify the cursor for reading/writing the file as a stream
  • read_at always read as much as possible. When its return value is smaller than requested length, it always indicates EOF
  • write_at is forbidden on files opened with append=true; use sequential writes to append data

#Directory Operations

#Creating Directories

///|
#cfg(target="native")
async test "mkdir - create directory" {
let dir_path = "_build/test_mkdir"
@fs.mkdir(dir_path, permission=0o755)
let exists = @fs.exists(dir_path)
@fs.rmdir(dir_path)
inspect(exists, content="true")
}

///|
#cfg(target="native")
async test "mkdir - create with custom permissions" {
let dir_path = "_build/test_mkdir_perm"
@fs.mkdir(dir_path, permission=0o700)
let kind = @fs.kind(dir_path)
@fs.rmdir(dir_path)
debug_inspect(kind, content="Directory")
}

#Reading Directories

///|
#cfg(target="native")
async test "readdir - read directory entries" {
let dir_path = "_build/test_readdir"
@fs.mkdir(dir_path, permission=0o755)
@fs.write_file("\{dir_path}/test1.txt", b"", create_mode=CreateOrTruncate)
@fs.write_file("\{dir_path}/test2.txt", b"", create_mode=CreateOrTruncate)
let entries = @fs.readdir(
dir_path,
include_hidden=false,
include_special=false,
)
// avoid platform inconsistent ordering
entries.sort()
@fs.rmdir(dir_path, recursive=true)
json_inspect(entries, content=["test1.txt", "test2.txt"])
}

///|
#cfg(target="native")
async test "readdir with sorting" {
let dir_path = "_build/test_readdir_sort"
@fs.mkdir(dir_path, permission=0o755)
@fs.write_file("\{dir_path}/c.txt", b"", create_mode=CreateOrTruncate)
@fs.write_file("\{dir_path}/a.txt", b"", create_mode=CreateOrTruncate)
@fs.write_file("\{dir_path}/b.txt", b"", create_mode=CreateOrTruncate)
let entries = @fs.readdir(dir_path, sort=true)
@fs.rmdir(dir_path, recursive=true)
json_inspect(entries, content=["a.txt", "b.txt", "c.txt"])
}

///|
#cfg(target="native")
async test "opendir and Directory::read_all" {
let dir_path = "_build/test_opendir"
@fs.mkdir(dir_path, permission=0o755)
@fs.write_file("\{dir_path}/file1.txt", b"test", create_mode=CreateOrTruncate)
@fs.write_file("\{dir_path}/file2.txt", b"test", create_mode=CreateOrTruncate)
let dir = @fs.opendir(dir_path)
let entries = dir
.read_all(include_hidden=false, include_special=false)
..sort()
dir.close()
@fs.rmdir(dir_path, recursive=true)
json_inspect(entries, content=["file1.txt", "file2.txt"])
}

#Walking Directory Trees

Recursively traverse directory hierarchies:

///|
#cfg(target="native")
async test "walk directory tree" {
let base = "_build/test_walk"
@fs.mkdir(base)
@fs.mkdir("\{base}/sub1")
@fs.mkdir("\{base}/sub2")
@fs.write_file("\{base}/file.txt", b"", create_mode=CreateOrTruncate)
@fs.write_file("\{base}/sub1/file1.txt", b"", create_mode=CreateOrTruncate)
let visited : Ref[Int] = Ref(0)
@fs.walk(base, fn(_path, _files) { visited.val = visited.val + 1 })
@fs.remove("\{base}/file.txt")
@fs.remove("\{base}/sub1/file1.txt")
@fs.rmdir("\{base}/sub1")
@fs.rmdir("\{base}/sub2")
@fs.rmdir(base)
inspect(visited.val >= 3, content="true")
}

///|
#cfg(target="native")
async test "walk with max_concurrency" {
let base = "_build/test_walk_concurrency"
@fs.mkdir(base, permission=0o755)
@fs.mkdir("\{base}/dir1", permission=0o755)
@fs.mkdir("\{base}/dir2", permission=0o755)
let count : Ref[Int] = Ref(0)
@fs.walk(
base,
fn(_path, _files) { count.val = count.val + 1 },
max_concurrency=1,
)
@fs.rmdir("\{base}/dir1")
@fs.rmdir("\{base}/dir2")
@fs.rmdir(base)
inspect(count.val >= 3, content="true")
}

#Removing Directories

///|
#cfg(target="native")
async test "rmdir - remove empty directory" {
let dir_path = "_build/test_rmdir"
@fs.mkdir(dir_path)
@fs.rmdir(dir_path)
let exists = @fs.exists(dir_path)
inspect(exists, content="false")
}

///|
#cfg(target="native")
async test "rmdir recursive - remove directory tree" {
let base = "_build/test_rmdir_recursive"
@fs.mkdir(base)
@fs.mkdir("\{base}/subdir")
@fs.write_file("\{base}/file.txt", b"test", create_mode=CreateOrTruncate)
@fs.write_file(
"\{base}/subdir/nested.txt",
b"test",
create_mode=CreateOrTruncate,
)
@fs.rmdir(base, recursive=true)
let exists = @fs.exists(base)
inspect(exists, content="false")
}

#File Metadata

#File Kind

Determine the type of file system entries:

///|
#cfg(target="native")
async test "kind - regular file" {
let test_file = "_build/test_kind_file.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let kind = @fs.kind(test_file)
@fs.remove(test_file)
debug_inspect(kind, content="Regular")
}

///|
#cfg(target="native")
async test "kind - directory" {
let dir_path = "_build/test_kind_dir"
@fs.mkdir(dir_path, permission=0o755)
let kind = @fs.kind(dir_path)
@fs.rmdir(dir_path)
debug_inspect(kind, content="Directory")
}

///|
#cfg(target="native")
async test "File::kind method" {
let test_file = "_build/test_file_kind.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let file = @fs.open(test_file, mode=ReadOnly)
let kind = file.kind()
file.close()
@fs.remove(test_file)
debug_inspect(kind, content="Regular")
}

#Timestamps

Access file timestamps (atime, mtime, ctime):

///|
#cfg(all(target="native", not(platform="windows")))
async test "atime - access time" {
let test_file = "_build/test_atime.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let (seconds, nanoseconds) = @fs.atime(test_file)
@fs.remove(test_file)
inspect(seconds > 0, content="true")
inspect(nanoseconds >= 0, content="true")
}

///|
#cfg(target="native")
async test "mtime - modification time" {
let test_file = "_build/test_mtime.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let (seconds, nanoseconds) = @fs.mtime(test_file)
@fs.remove(test_file)
inspect(seconds > 0, content="true")
inspect(nanoseconds >= 0, content="true")
}

///|
#cfg(target="native")
async test "ctime - status change time" {
let test_file = "_build/test_ctime.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let (seconds, nanoseconds) = @fs.ctime(test_file)
@fs.remove(test_file)
inspect(seconds > 0, content="true")
inspect(nanoseconds >= 0, content="true")
}

///|
#cfg(target="native")
async test "File timestamp methods" {
let test_file = "_build/test_file_times.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let file = @fs.open(test_file, mode=ReadOnly)
let (atime_s, _) = file.atime()
let (mtime_s, _) = file.mtime()
let (ctime_s, _) = file.ctime()
file.close()
@fs.remove(test_file)
inspect(atime_s > 0, content="true")
inspect(mtime_s > 0, content="true")
inspect(ctime_s > 0, content="true")
}

#File Permissions

Check file access permissions:

///|
#cfg(target="native")
async test "exists - check file existence" {
let test_file = "_build/test_exists.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let exists = @fs.exists(test_file)
@fs.remove(test_file)
inspect(exists, content="true")
}

///|
#cfg(target="native")
async test "exists - non-existent file" {
let exists = @fs.exists("nonexistent_file_xyz.txt")
inspect(exists, content="false")
}

///|
#cfg(target="native")
async test "can_read - check read permission" {
let test_file = "_build/test_can_read.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let can_read = @fs.can_read(test_file)
@fs.remove(test_file)
inspect(can_read, content="true")
}

///|
#cfg(target="native")
async test "can_write - check write permission" {
let test_file = "_build/test_can_write.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
let can_write = @fs.can_write(test_file)
@fs.remove(test_file)
inspect(can_write, content="true")
}

///|
#cfg(target="native")
async test "can_execute - check execute permission" {
let test_file = "_build/test_can_execute.txt"
@fs.write_file(test_file, b"test", create_mode=CreateNew, permission=0o755)
let can_execute = @fs.can_execute(test_file)
@fs.remove(test_file)
inspect(can_execute, content="true")
}

#Path Operations

///|
#cfg(target="native")
async test "realpath - resolve absolute path" {
let test_dir = "_build/test_realpath"
@fs.mkdir(test_dir)
let real_path = @fs.realpath(test_dir)
@fs.rmdir(test_dir)
@env.current_dir() is Some(cwd)
assert_true(real_path.has_prefix(cwd))
inspect(
// replace `\\` with `/` for Windows
real_path[cwd.length():].replace_all(old="\\", new="/"),
content="/_build/test_realpath",
)
}

#File Removal

///|
#cfg(target="native")
async test "remove - delete file" {
let test_file = "_build/test_remove.txt"
@fs.write_file(test_file, b"test", create_mode=CreateOrTruncate)
@fs.remove(test_file)
let exists = @fs.exists(test_file)
inspect(exists, content="false")
}

#File Watching

Watcher watches a directory tree recursively and reports file system changes as FsEvent values. Events use paths relative to the watched directory, with / as the path separator. New files and directories are added to the watched tree automatically, and removed entries are unwatched automatically. Calling .wait() on the watcher will block and wait until the watched tree has changed since the last .wait() or .wait_any() call. A list of events describing the net changes on the watched tree since the last query will be returned. Note that events returned by .wait() report net change rather than detailed transaction. So for example a create event followed by a remove event on the same location will cancel each other. If the user only cares about when the watched tree change, and does not care about the detailed list of changes, .wait_any() can be used, which is slightly faster than .wait().

The watcher performs aggresive global rename detection using the physical identity of files. There are several cases where the watcher will not report rename event though:

  • renaming of directories are only reported as Rename when the renaming happen within the same parent directory
  • some rename sequences cannot be serialized as binary Rename, such as swapping two files

In these cases, the rename will be split into separated Remove and Create events.

The following options are available on watcher creation:

  • File system notifications are often delivered in bursts, so the watcher debounces changes by default. The debounce behavior can be configured by the debounce_timeout and max_debounce_delay options on watcher creation. The watcher will wait until no event happens for debounce_timeout milliseconds before reporting any events, but the total wait time will never exceed max_debounce_delay milliseconds.
  • ignored_paths, if present, can be used to filter out paths that the user don't want to watch. When a file or directory is going to be watched, its path (relative to root of watched tree, using / as path separator, always without trailing /) will be supplied to ignored_paths. If ignored_paths return true, the file or directory will be ignored. When a directory is ignored, all files/directories within it are also ignored. So to ignore a single directory, ignored_paths only need to handle paths of the files inside that ignored directory.
  • When a create/remove event is reported for a directory:
    • if report_child_event=true, respective create/remove events will be emitted for everything inside that directory. This mode is useful if tracking the exact list of files in desirable.
    • If report_child_event=false (the default), only a single event for the directory itself will be emitted, making the watcher less noisy
  • If report_event_on_init=true (false by default), the first wait call will return immediately after watcher creation, reporting events describing the initial structure of the watched directory. This is useful for keeping the knowledge of the caller in sync with the watcher. Note that you probably want to set report_child_event=true as well in this case.

///|
#cfg(target="native")
async fn watch_project_sources() -> Unit {
let watcher = @fs.Watcher("src", ignored_paths=path => {
path.has_prefix("_build/")
})
defer watcher.close()
while watcher.wait() is events {
debug(events)
}
}

#Types Reference

#FileKind

Represents the type of a file system entry:

  • Regular - Regular file
  • Directory - Directory
  • SymLink - Symbolic link
  • Socket - Unix socket
  • Pipe - Named pipe (FIFO)
  • BlockDevice - Block device
  • CharDevice - Character device
  • Unknown - Unknown file type

#Mode

File opening mode:

  • ReadOnly - Open for reading only
  • WriteOnly - Open for writing only
  • ReadWrite - Open for both reading and writing

#SyncMode

Data synchronization mode:

  • NoSync - No synchronization (default)
  • Data - Sync data and essential metadata (like file size)
  • Full - Sync all data and metadata

#SeekMode

How to interpret seek offsets:

  • FromStart - Absolute offset from start of file
  • FromEnd - Offset relative to end of file
  • Relative - Offset relative to current position

#File

Main file handle type. Methods include:
  • fd() - Get file descriptor
  • close() - Close file
  • read() - Read data into buffer
  • read_all() - Read entire file content
  • read_exactly() - Read exact number of bytes
  • write() - Write data
  • write_once() - Single write operation
  • read_at() - Read file at specified position
  • read_exact_at() - Read exect number of bytes at specified position
  • write_at() - Write file at specified position
  • size() - Get file size
  • kind() - Get file kind
  • atime(), mtime(), ctime() - Get timestamps

#Directory

Directory handle for reading directory entries:
  • close() - Close directory
  • read_all() - Read all entries

#FsEvent

FsEvent describes a net change reported by Watcher::wait. All paths are relative to the watched directory and use / as the path separator.

  • Modify(path) reports that the regular file at path has been modified.
  • Create(path) reports that a file or directory now exists at path.
  • Remove(path) reports that the file or directory at path has been removed.
  • Rename(old~, new~) reports that a file or directory moved from old to new.

#Best Practices

  1. Always close files: Use defer file.close() after opening files
  2. Use appropriate sync modes: NoSync for performance, Data or Full for durability
  3. Handle permissions: Specify appropriate UNIX permissions (e.g., 0o644 for files, 0o755 for directories)
  4. Check existence: Use exists() before performing operations on files
  5. Use convenience functions: read_file() and write_file() are simpler for common cases
  6. Walk with limits: Use max_concurrency parameter when walking large directory trees

#Error Handling

All async file operations can raise errors. Use proper error handling:

///|
#cfg(target="native")
async test "error handling example" {
@test_util.assert_raise_async(() => @fs.read_file("nonexistent.txt"))
}

For more examples and detailed usage, see the individual test files in this package.

#
CreateMode

pub(all) enum CreateMode {
OpenExisting
TruncateExisting
OpenOrCreate
CreateOrTruncate
CreateNew
}

Specify how to handle file creation in @fs.open().

  • OpenExisting: open an existing file, fail if the file does not exist
  • TruncateExisting: open an existing file and truncate it, fail if the file does not exist
  • OpenOrCreate: if the file exists, open it without truncation. Otherwise create a new file
  • CreateOrTruncate: if the file exists, open it and truncate it. Otherwise create a new file
  • CreateNew: create a new file, fail if the file already exists

#
Directory

type Directory

A directory in file system

#
Directory::close

fn Directory::close(self : Directory) -> Unit

#
Directory::next

async fn Directory::next(dir : Directory, include_hidden? : Bool, include_special? : Bool) -> DirectoryEntry?

Fetch the next entry in the directory, including its name and some extra information, see DirectoryEntry. None indicates that the directory has no more files.

  • If include_special is true (false by default), . (current directory) and .. (parent directory) will be included too

  • If include_hidden is true (true by default), hidden files (on Unix: file name starts with ., on Windows: has the hidden attribute) will be included

The file name in the returned directory entry is interpreted as UTF8-encoded on Unix-like systems.

#
Directory::read_all

async fn Directory::read_all(dir : Directory, include_hidden? : Bool, include_special? : Bool) -> Array[String]

Read all entries in a directory.

  • If include_special is true (false by default), . (current directory) and .. (parent directory) will be included too

  • If include_hidden is true (true by default), hidden files (on Unix: file name starts with ., on Windows: has the hidden attribute) will be included

The returned file names are interpreted as UTF8-encoded on Unix-like systems.

#
DirectoryEntry

pub struct DirectoryEntry {
name : String
is_dir : Bool
// private fields
}

#
File

type File

impl Reader for File
impl Writer for File

#
File::as_dir

fn File::as_dir(self : File) -> Directory raise

Convert a file to directory. If the file is not a directory, an error will be raised.

The ownership of the file will be transferred to as_dir. The input file will be automatically closed when the result directory object is closed. If as_dir fails, the input file will be closed automatically.

#
File::atime

async fn File::atime(self : File) -> (Int64, Int)

Get the last access time of a file. The return value is a pair (s, ns), representing the time of s seconds + ns nanoseconds.

#
File::close

fn File::close(self : File) -> Unit

#
File::ctime

async fn File::ctime(self : File) -> (Int64, Int)

Get the last status change time of a file. The return value is a pair (s, ns), representing the time of s seconds + ns nanoseconds.

#
File::fd

fn File::fd(self : File) -> Int

#
File::kind

fn File::kind(self : File) -> FileKind

#
File::lock

async fn File::lock(self : File, lock : Lock) -> Unit

Lock a file, block until the lock is successfully acquired.

The lock here is advisory, so normal read/write operations on the file are not affected by the lock, only another attempt to lock the same file may conflict with the lock here.

The underlying syscall used to implement the lock is undefined. So the lock should only be used for synchronization between multiple process written using moonbitlang/async.

#
File::mtime

async fn File::mtime(self : File) -> (Int64, Int)

Get the last modification time of a file. The return value is a pair (s, ns), representing the time of s seconds + ns nanoseconds.

#
File::read_at

async fn File::read_at(self : File, buf : FixedArray[Byte], position~ : Int64, offset? : Int, len? : Int) -> Int

Read from a file at the offset given by position. Only seekable files (e.g. regular files and block devices) support this operation, calling read_at on unsupported file, such as pipe or socket, result in error.

This function does not change the cursor for reading/writing the file as a stream (i.e. using API based on @io.Reader and @io.Writer), so it is safe to perform multiple read_at on the same file simutaneously.

Up to len bytes of data will be read into buf[offset:]. The number of bytes actually read would be returned. The number of read bytes will be smaller than len only when EOF is reached.

#
File::read_exactly_at

async fn File::read_exactly_at(self : File, len : Int, position~ : Int64) -> Bytes

#
File::size

async fn File::size(self : File) -> Int64

Get the size of the file. This method will not change position in the file. Can only be applied to a regular file.

#
File::sync

async fn File::sync(self : File, only_data? : Bool) -> Unit

Flush all in-memory data of an opened file to disk. If only_data is true (false by default), some metadata such as timestamp may not be flushed. Buf file content and important metadata such as file size are always flushed.

#
File::try_lock

fn File::try_lock(self : File, lock : Lock) -> Bool raise

Try to lock a file. If the lock is successfully acquired, true is returned. If the lock cannot be acquired immediately (because an incompatible lock is held by another process), false is returned immediately.

The lock here is advisory, so normal read/write operations on the file are not affected by the lock, only another attempt to lock the same file may conflict with the lock here.

The underlying syscall used to implement the lock is undefined. So the lock should only be used for synchronization between multiple process written using moonbitlang/async.

#
File::unlock

fn File::unlock(self : File) -> Unit

Unlock a file that was previously locked by this process.

#
File::write_at

async fn File::write_at(self : File, buf : BytesView, position~ : Int64) -> Unit

Write to a file at the offset given by position. Only seekable files (e.g. regular files and block devices) support this operation, calling write_at on unsupported file, such as pipe or socket, result in error.

This function does not change the cursor for reading/writing the file as a stream (i.e. using API based on @io.Reader and @io.Writer), so it is safe to perform multiple write_at on the same file simutaneously, as long as the regions they write do not overlap.

write_at would only return after all data is written.

#
FileKind

pub(all) enum FileKind {
Unknown
Regular
Directory
SymLink
Socket
Pipe
BlockDevice
CharDevice
} derive(Eq,
Debug
)

impl Show for FileKind

#
FsEvent

pub(all) enum FsEvent {
Modify(String)
Create(String)
Remove(String)
Rename(old~ : String, new~ : String)
} derive(Eq,
Debug
)

A file system event generated by @fs.Watcher.

  • Modify(path): the regular file at path has been modified
  • Create(path): a new file or directory is created at path
  • Remove(path): the file or directory at path is removed
  • Rename(old~, new~), the file previously at old has been moved to new

All paths are relative to the base directory of the watcher, using / as path separator.

#
Lock

pub(all) enum Lock {
Shared
Exclusive
}

The kind of lock that can be acquired on a file. Exclusive lock must be unique, while multiple Shared lock can coexist. Exclusive lock and Shared lock are mutually exclusive.

#
Mode

pub(all) enum Mode {
ReadOnly
WriteOnly
ReadWrite
}

#
SyncMode

pub(all) enum SyncMode {
NoSync
Data
Full
}

Determine how data is synchronized after writing:

  • NoSync means no synchronization is performed.

  • Data means the data part and relevant metadata such as file size will be synchronized immediately after a write call, but other metadata, such as last access time, will not. See man page of fdatasync(2) for more details.

  • Full means all data and metdata will be synchronized immediately after writing. See man page of fsync(2) for more details.

#
Watcher

type Watcher

A file system watcher for watching changes in a directory.

#
Watcher::Watcher

async fn Watcher::Watcher(path : String, debounce_timeout? : Int, max_debounce_delay? : Int, report_child_event? : Bool, report_event_on_init? : Bool, ignored_paths? : (String) -> Bool) -> Watcher

Create a new file system watcher that watches the directory path recursively. Currently only recursive directory watching is supported.

When files are created, removed, modified or renamed inside path, the watcher will get notified and report events for the change. New files will be watched automatically, and removed files will be unwatched automatically.

File system events often come in batch. For example, removing a directory recursively will result in several removal event in a row. By default Watcher will perform debouncing internally: when many events are happening in sequence, instead of immediately return on the first event, Watcher::wait_any will wait until no event is happening within the last debounce_timeout ms (defaults to 20ms), or if max_debounce_delay ms (defaults to 200ms) has elapsed since the first event.

ignored_paths, if present, can be used to filter out paths that the user don't want to watch. When a file or directory is going to be watched, its path (relative to root of watched tree, using / as path separator) will be supplied to ignored_paths. If ignored_paths return true, the file or directory will be ignored. When a directory is ignored, all files/directories within it are also ignored. So to ignore a single directory, ignored_paths only need to handle paths of the files inside that ignored directory.

When a create/remove event is reported for a directory:
  • if report_child_event=true, respective create/remove events will be emitted for everything inside that directory. This mode is useful if tracking the exact list of files in desirable.
  • If report_child_event=false (the default), only a single event for the directory itself will be emitted, making the watcher less noisy

If report_event_on_init=true (false by default), the first wait call will return immediately after watcher creation, reporting events describing the initial structure of the watched directory. This is useful for keeping the knowledge of the caller in sync with the watcher. Note that you probably want to set report_child_event=true as well in this case.

Currently, the behavior when path itself is renamed or removed is undefined.

#
Watcher::close

fn Watcher::close(self : Watcher) -> Unit

#
Watcher::wait

async fn Watcher::wait(self : Watcher) -> Array[FsEvent]

Wait for any change in the watched tree, and return a list of events describing what changed since the last wait/wait_any call or watcher creation.

The returned list describe the net change of the watched tree since the last query, instead of recording a precise list of transactions that happen since the last wait. For example, a create event followed by a removed event on the same path will get fused and result in nothing in the returned event list.

wait performs rename detection based on the physical ID of files. The rule is:
  • renaming for regular files are detected globally for the whole watched tree
  • renaming for directory are only detected locally within the same parent directory. Directory renaming across directory are treated as separated remove + create events.
  • if the destination of a rename already contains something, a remove event will be emitted for the old file on the destination. When the watcher cannot serialize the file system changes into simple rename events, for example two files are atomically swapped, a remove event on the old path + a create event on the new path will be created.

This method is cancellation safe. When it get cancelled and raises the canellation error, it is guaranteed that no event will be consumed.

#
Watcher::wait_any

async fn Watcher::wait_any(self : Watcher) -> Unit

Wait for any change in the watched tree since the last wait/wait_any call or watcher creation.

This method is cancellation safe. When it get cancelled and raises the canellation error, it is guaranteed that no event will be consumed.

#
atime

async fn atime(path : StringView, follow_symlink? : Bool) -> (Int64, Int)

Get the last access time of a file at path. The return value is a pair (s, ns), representing the time of s seconds + ns nanoseconds.

If follow_symlink is true (true by default) and path is a symbolic link, the timestamp of the target of path will be returned.

#
can_execute

async fn can_execute(path : StringView) -> Bool

#
can_read

async fn can_read(path : StringView) -> Bool

#
can_write

async fn can_write(path : StringView) -> Bool

#
chmod

async fn chmod(path : StringView, mode : Int) -> Unit

Change the permission of a file. Permission is represented as an integer in UNIX permission style. For example, 0o640 means:

  • the owner of the file can read and write the file (6)
  • users in the owner group of the file can read the file (4)
  • other users can do nothing to the file

On Windows, @fs.chmod is not supported, and will fail with error.

#
create

async fn create(filename : StringView, allow_existing? : Bool, permission? : Int, sync? : SyncMode) -> File

Create a new file at pathname. pathname will be encoded using UTF8.

  • If the file already exists and allow_existing is true (true by default), the existing file will be truncated and opened. If the file already exists and allow_existing is fale, this function will fail.

  • User permission of the new file would be set to permission, which is an integer in UNIX style. For example, 0o640 means:
    • the owner of the file can read and write the file (6)
    • users in the owner group of the file can read the file (4)
    • other users can do nothing to the file The default value is 0o644 (anyone can read, only owner can write, not executable). permission is currently ignored on Windows.

  • sync determines how data will be synchronized to disk after writing, see SyncMode for more details. The default is NoSync

#
ctime

async fn ctime(path : StringView, follow_symlink? : Bool) -> (Int64, Int)

Get the last status change time of a file at path. The return value is a pair (s, ns), representing the time of s seconds + ns nanoseconds.

If follow_symlink is true (true by default) and path is a symbolic link, the timestamp of the target of path will be returned.

#
exists

async fn exists(path : StringView) -> Bool

#
kind

async fn kind(path : StringView, follow_symlink? : Bool) -> FileKind

Get the kind of a file at path. If path is a symbolic link and follow_symlink is true (true by default), the kind of the target of the link will be returned.

#
mkdir

async fn mkdir(path : StringView, permission? : Int, recursive? : Bool) -> Unit

Create a directory at path.

The Unix-style permission of the created directory can be set in permission, the default value is 0o755 (anyone can read and traverse, only owner can write). The permission parameter is currently ignored on Windows.

If recursive=true (false by default), non-existing parent directories of path will be recursively created, too.

#
mtime

async fn mtime(path : StringView, follow_symlink? : Bool) -> (Int64, Int)

Get the last modification time of a file at path. The return value is a pair (s, ns), representing the time of s seconds + ns nanoseconds.

If follow_symlink is true (true by default) and path is a symbolic link, the timestamp of the target of path will be returned.

#
open

async fn open(filename : StringView, mode~ : Mode, sync? : SyncMode, append? : Bool, create_mode? : CreateMode, permission? : Int, create? : Int, truncate? : Bool) -> File

Open a file. filename will be encoded using UTF8.

  • mode determines what operations (read/write) are permitted for the file.

  • sync determines how data will be synchronized to disk after writing, see SyncMode for more details. The default is NoSync

  • if append is true (false by default), the file will be opened in append mode. Under append mode, sequential write (via the @io.Writer interface) will always write to the end of the file, even if the file is modified externally. The semantic of reading from the file is unaffected by append. Random access write (write_at) is forbidden for file opened in append mode.

  • create_mode specify how to handle file creation, truncation, etc. See the type CreateMode for more details. The default value is OpenExisting.

  • permission specifies the user permission of the new file, if a new file is created by @fs.open. The permission is represented as an integer in UNIX permission style. For example, 0o640 means:

    • the owner of the file can read and write the file (6)
    • users in the owner group of the file can read the file (4)
    • other users can do nothing to the file

    The default value is 0o644 (anyone can read, only owner can write, not executable). permission is currently ignored on Windows.

#
opendir

async fn opendir(path : StringView) -> Directory

Open the directory at path. path is encoded UTF8. If path is not a directory, an error will be raised

#
read_file

async fn read_file(path : StringView, sync_timestamp? : Bool) -> &
Data

Read the contents of the file located at path. If sync_timestamp is true (false by default), read_file will block until timestamp change is written to the file system.

#
readdir

async fn readdir(path : StringView, include_hidden? : Bool, include_special? : Bool, sort? : Bool) -> Array[String]

Read all entries in the directory located at path.

  • If include_special is true (false by default), . (current directory) and .. (parent directory) will be included too

  • If include_hidden is true (true by default), hidden files (on Unix: file name starts with ., on Windows: has the hidden attribute) will be included

  • If sort is true (false by default), the result will be sorted using Array::sort.

The returned file names are interpreted as UTF8-encoded on Unix-like systems.

#
realpath

async fn realpath(path : StringView) -> String

Get the absolute real path of path, removing all .. and . in the path, and unfold all symbolic links.

If the path contains cyclic symbolic link, an error will be raised.

#
remove

async fn remove(path : StringView) -> Unit

Remove the file located at path. path is encoded using UTF8.

#
rename

async fn rename(old_path : StringView, new_path : StringView, replace? : Bool) -> Unit

Rename (move) the file located at old_path to new_path.

If replace is true (true by default), and a file already exist on new_path, that file will be replaced. In this case, open handles to the existing file remain valid, and still point to the old file.

#
rmdir

async fn rmdir(path : StringView, recursive? : Bool) -> Unit

Remove a directory at path, If recursive is true (false by default), files and directories in path will be removed recursively. If recursive is false, path must be an empty directory, otherwise rmdir will fail.
async fn symlink(target~ : StringView, path : StringView, force_symlink? : Bool) -> Unit

Create a symbolic link to old_path at new_path.

On Windows, creating true symbolic link requires admin privilege. So @fs.symlink() will try to create NTFS junction when feasible, which is similar to symlink in characteristics. NTFS junction is not as flexible as symlink, it will only be created when:

  1. the target path is an existing directory
  2. the target path is onn the same local machine
  3. the link is created on a NTFS volume

To force creation of true symbolic link on Windows, pass force_symlink=true. force_symlink has no effect on non-Windows platforms.

#
tmpdir

async fn tmpdir(prefix~ : StringView) -> String

Create a temporary directory in the system-specific temporary file storage (such as /tmp/) with prefix prefix. tmpdir force-create the directory atomically, so it never reuse directory.

The format of the directory name is:

.<current_pid>.<random_number>

The random number part cotains 32bits of information, so at most 2^32 temporary directories can be created for every process. However, due to collision, tmpdir will slow down when a lot of temporary directories are created by the same process.

The temporary directory name is not cryptographically secure.

#
walk

async fn walk(path : StringView, f : async (String, Array[String]) -> Unit, exclude? : (String) -> Bool, max_concurrency? : Int, allow_failure? : Bool) -> Unit

walk(path, f, ..) recursively walk the directory path, and call f on every sub-directory in path, including path itself.

If exclude is present, it will be invoked with the path of every directory before traversing them. If exclude returns true, the directory and all its children will be skipped. The path passed to exclude has the following format:
  • always start with path
  • does not contain trailing directory separator

The directories are walked in parallel. Use max_concurrency to limit the number of workers spawned in parallel. For example, setting max_concurrency=1 result in sequential walking. By default max_concurrency is set to a large value.

If allow_failure is true (false by default), error raised by f will be silently ignored. Otherwise, failure in f will stop the whole walk, cancelling the handling of other sub-directories.

#
write_file

async fn write_file(path : StringView, content : &
Data
, create_mode? : CreateMode, permission? : Int, sync? : SyncMode, append? : Bool, create? : Int, truncate? : Bool) -> Unit

Write data to a file located at path. The meaning of sync, append, create_mode and permission, is the same as open, except that create_mode is TruncateExisting by default. See open for more details.