#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!")
    defer @fs.remove(test_file)
    let file = @fs.open(test_file, mode=ReadOnly)
    defer file.close()
    let content = file.read_all().text()
    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 @fs.remove(test_file)
    defer file.close()
    file.write(b"Hello, World!")
    }

    ///|
    #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")
    defer @fs.remove(test_file)

    // Append to existing file
    {
    let file = @fs.open(test_file, mode=WriteOnly, append=true)
    defer file.close()
    file.write(b"Second line\n")
    }
    let content = @fs.read_file(test_file).text()
    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)
    defer @fs.remove(test_file)
    {
    defer file.close()
    file.write(b"New file content")
    }
    let exists = @fs.exists(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!")
    defer @fs.remove(test_file)
    let content = @fs.read_file(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")
    defer @fs.remove(test_file)
    let file = @fs.open(test_file, mode=ReadOnly)
    defer file.close()
    let buf = FixedArray::make(5, b'0')
    let n = file.read(buf)
    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")
    defer @fs.remove(test_file)
    let file = @fs.open(test_file, mode=ReadOnly)
    defer file.close()
    let data = file.read_all()
    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")
    defer @fs.remove(test_file)
    let file = @fs.open(test_file, mode=ReadOnly)
    defer file.close()
    let bytes = file.read_exactly(5)
    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")
    defer @fs.remove(test_file)
    let content = @fs.read_file(test_file).text()
    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)
    defer @fs.remove(test_file)
    let content = @fs.read_file(test_file).text()
    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)
    defer @fs.remove(test_file)
    {
    defer file.close()
    file.write(b"Line 1\n")
    file.write(b"Line 2\n")
    }
    let content = @fs.read_file(test_file).text()
    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)
    defer @fs.remove(test_file)
    defer file.close()
    let data : Bytes = b"Single write"
    let written = file.write_once(data, offset=0, len=data.length())
    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")
    defer @fs.remove(test_file)
    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")
    }

    ///|
    #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 @fs.remove(test_file)
    {
    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")
    }

    ///|
    #cfg(target="native")
    async test "size - get file size" {
    let test_file = "_build/test_size.txt"
    @fs.write_file(test_file, b"Hello")
    defer @fs.remove(test_file)
    let file = @fs.open(test_file, mode=ReadOnly)
    defer file.close()
    let size = file.size()
    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)
    defer @fs.rmdir(dir_path)
    let exists = @fs.exists(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)
    defer @fs.rmdir(dir_path)
    let kind = @fs.kind(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)
    defer @fs.rmdir(dir_path, recursive=true)
    @fs.write_file("\{dir_path}/test1.txt", b"")
    @fs.write_file("\{dir_path}/test2.txt", b"")
    let entries = @fs.readdir(
    dir_path,
    include_hidden=false,
    include_special=false,
    )
    // avoid platform inconsistent ordering
    entries.sort()
    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)
    defer @fs.rmdir(dir_path, recursive=true)
    @fs.write_file("\{dir_path}/c.txt", b"")
    @fs.write_file("\{dir_path}/a.txt", b"")
    @fs.write_file("\{dir_path}/b.txt", b"")
    let entries = @fs.readdir(dir_path, sort=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)
    defer @fs.rmdir(dir_path, recursive=true)
    @fs.write_file("\{dir_path}/file1.txt", b"test")
    @fs.write_file("\{dir_path}/file2.txt", b"test")
    let dir = @fs.opendir(dir_path)
    defer dir.close()
    let entries = dir
    .read_all(include_hidden=false, include_special=false)
    ..sort()
    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)
    defer @fs.rmdir(base, recursive=true)
    @fs.mkdir("\{base}/sub1")
    @fs.mkdir("\{base}/sub2")
    @fs.write_file("\{base}/file.txt", b"")
    @fs.write_file("\{base}/sub1/file1.txt", b"")
    let visited : Ref[Int] = Ref(0)
    @fs.walk(base, fn(_path, _files) { visited.val = visited.val + 1 })
    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)
    defer @fs.rmdir(base, recursive=true)
    @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,
    )
    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")
    @fs.write_file("\{base}/subdir/nested.txt", b"test")
    @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")
    defer @fs.remove(test_file)
    let kind = @fs.kind(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)
    defer @fs.rmdir(dir_path)
    let kind = @fs.kind(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")
    defer @fs.remove(test_file)
    let file = @fs.open(test_file, mode=ReadOnly)
    defer file.close()
    let kind = file.kind()
    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")
    defer @fs.remove(test_file)
    let (seconds, nanoseconds) = @fs.atime(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")
    defer @fs.remove(test_file)
    let (seconds, nanoseconds) = @fs.mtime(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")
    defer @fs.remove(test_file)
    let (seconds, nanoseconds) = @fs.ctime(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")
    defer @fs.remove(test_file)
    let file = @fs.open(test_file, mode=ReadOnly)
    defer file.close()
    let (atime_s, _) = file.atime()
    let (mtime_s, _) = file.mtime()
    let (ctime_s, _) = file.ctime()
    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")
    defer @fs.remove(test_file)
    let exists = @fs.exists(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")
    defer @fs.remove(test_file)
    let can_read = @fs.can_read(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")
    defer @fs.remove(test_file)
    let can_write = @fs.can_write(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)
    defer @fs.remove(test_file)
    let can_execute = @fs.can_execute(test_file)
    inspect(can_execute, content="true")
    }

    #File Size

    ///|
    #cfg(target="native")
    async test "get file size by path" {
    let test_file = "_build/test_size_by_path.txt"
    @fs.write_file(test_file, b"Hello")
    inspect(@fs.file_size(test_file), content="5")
    }

    #Path Operations

    ///|
    #cfg(target="native")
    async test "realpath - resolve absolute path" {
    let test_dir = "_build/test_realpath"
    @fs.mkdir(test_dir)
    defer @fs.rmdir(test_dir)
    let real_path = @fs.realpath(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")
    defer (if @fs.exists(test_file) { @fs.remove(test_file) })
    @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::drop

    async fn File::drop(self : File, len : Int) -> Int

    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

    async fn File::read(self : File, dst : FixedArray[Byte], offset? : Int, max_len? : Int) -> Int

    File::read_all

    async fn File::read_all(self : File) -> &
    Data

    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

    async fn File::read_exactly(self : File, len : Int) -> Bytes

    File::read_exactly_at

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

    File::read_some

    async fn File::read_some(self : File, max_len? : Int) -> Bytes?

    File::read_until

    async fn File::read_until(self : File, sep : StringView) -> String?

    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

    async fn File::write(self : File, data : &
    Data
    ) -> Unit

    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.

    File::write_once

    async fn File::write_once(self : File, buf : Bytes, offset~ : Int, len~ : Int) -> Int

    File::write_reader

    async fn File::write_reader(self : File, reader : &
    Reader
    ) -> Unit

    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

    file_size

    async fn file_size(path : StringView) -> Int64

    Get the size of the file at the specific path. Can only be applied to a regular file.

    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, allow_exist? : 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. Parent directories are always created with allow_exist=true, so concurrent tasks may create directories under a shared parent.

    If allow_exist=true (false by default), it is not an error if path is already a directory (like mkdir -p). An existing non-directory at path is still an error.

    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) -> 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 nocancel

    Remove the file located at path. This function is not cancellable.

    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 nocancel

    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.

    This function is not cancellable.
    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) -> 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.