rokdocs
v0.1

3.1. fs:: - 파일 시스템

rok::std::fs 모듈은 파일과 디렉토리, link, 권한 등 파일 시스템 전반에 걸친 연산을 제공합니다. 설계의 출발점은 WASI Preview 2 의 capability-based 모델을 채택했다는 점이며, 그 결과 모든 op 는 Dir 핸들을 통해서만 수행됩니다.

3.1.0. 인자 규칙

  • 아래 시그니처의 경로 매개변수 Path 는 P: AsPath 의 약기입니다. str 과 Path 를 모두 받습니다 (3.8.3a).
  • 쓰기 함수의 데이터 매개변수 data 는 io::AsBytes (3.5) 를 받습니다. str, List<u8>, Slice<u8> 를 넘길 수 있습니다.

쓰는 쪽은 AsBytes 를 받고, 읽는 buffer 는 List<u8> 입니다 (read(buf) 가 채움).

3.1.1. 핵심 타입

핵심 타입은 OS 핸들을 감싸는 Dir 와 File, 디렉토리를 순회하기 위한 ReadDir 와 DirEntry, 그리고 메타데이터와 권한을 표현하는 Metadata 와 Permissions 로 구성됩니다. 파일을 열 때의 동작 방식은 OpenOptions 구조체로 세밀하게 제어합니다.

struct Dir { /* OS handle (file descriptor / HANDLE) */ }
struct File { /* OS handle */ }
struct ReadDir { /* iterator over Dir entries - Drop: using 해제 시 닫힘 */ }
struct DirEntry { /* path + metadata cache */ }
struct Metadata { /* size, 시각, perm, ... */ }
struct Permissions { /* readable/writable/executable + mode bits */ }

struct OpenOptions {
    read: bool;
    write: bool;
    append: bool;
    truncate: bool;
    create: bool;
    create_new: bool;
    mode: u32;          // unix permission (default 0o666)
}

3.1.2. Dir capability

Dir 는 capability 의 단위가 되는 타입으로, 프로세스 기본 cap 인 root() 외에도 작업 디렉토리, 홈, OS 임시 디렉토리를 각각 cwd(), home(), temp() 로 얻을 수 있습니다. 핸들을 손에 쥐고 나면 그 아래에서 파일을 열거나 디렉토리를 만들고 지우거나 이름을 바꾸는 등 거의 모든 파일 시스템 연산을 수행할 수 있고, 복사 시에는 copy_file_range / FICLONE / clonefile 과 같은 reflink 경로가 자동으로 활용됩니다.

impl Dir {
    fn root() -> Dir;                            // process default cap
    fn cwd() throws (IoError) -> Dir;            // current working dir
    fn home() throws (IoError) -> Dir;
    fn temp() throws (IoError) -> Dir;           // OS temp dir

    fn open(self, path: Path) throws (IoError) -> File;       // open with default OpenOptions::read
    fn open_with(self, path: Path, opts: OpenOptions) throws (IoError) -> File;
    fn open_dir(self, path: Path) throws (IoError) -> Dir;
    fn create(self, path: Path) throws (IoError) -> File;     // open + create + truncate

    fn read_dir(self) throws (IoError) -> ReadDir;            // using 으로 묶으면 해제 시 닫힘; let 으로 버리면 프로세스 끝까지 열림
    fn metadata(self, path: Path) throws (IoError) -> Metadata;
    fn exists(self, path: Path) -> bool;

    fn create_dir(self, path: Path) throws (IoError);
    fn create_dir_all(self, path: Path) throws (IoError);
    fn remove_file(self, path: Path) throws (IoError);
    fn remove_dir(self, path: Path) throws (IoError);
    fn remove_dir_all(self, path: Path) throws (IoError);
    fn rename(self, from: Path, to: Path) throws (IoError);

    fn copy(self, from: Path, to: Path) throws (IoError) -> u64;
                                          // reflink 를 먼저 시도 (macOS: clonefileat), 안 되면 바이트 복사
    fn symlink(self, target: Path, link: Path) throws (IoError);
    fn hardlink(self, target: Path, link: Path) throws (IoError);
}

내부적으로 Dir 는 openat 계열을 사용합니다. Dir::root() 가 아닌 Dir 은 절대 경로와 .. 를 담은 경로를 거부하므로, 그 디렉토리 아래로 접근이 한정됩니다. 이 검사는 경로 글자에 대한 것이므로, 디렉토리 안의 symlink 를 따라가면 바깥으로 나갈 수 있습니다. 완전한 격리가 필요하면 OS 의 sandbox 를 함께 씁니다.

3.1.3. File 메서드

File 은 일반적인 read / write 외에도 UTF-8 검증을 포함한 read_to_str, 디스크 동기화를 위한 sync_all / sync_data, 길이 조정을 위한 set_len 등을 갖추고 있습니다. 메타데이터와 권한을 조회하거나 변경하는 메서드도 제공되며, seek 와 try_clone 으로 fd 단위 조작도 가능합니다. Rust 1.77 (2024) 에서 안정화된 파일 잠금 API 와 메모리 매핑(mmap) 도 함께 제공됩니다.

impl File {
    fn read(self, buf: List<u8>) throws (IoError) -> usize;   // buf[0..n] 을 채움, 0 = 끝
    fn read_to_end(self) throws (IoError) -> List<u8>;
    fn read_to_str(self) throws (IoError) -> str;            // UTF-8 검증 포함
    fn write<B: AsBytes>(self, data: B) throws (IoError) -> usize;
    fn write_all<B: AsBytes>(self, data: B) throws (IoError);
    fn close(self) throws (IoError);

    fn flush(self) throws (IoError);
    fn sync_all(self) throws (IoError);                  // fsync (data + metadata)
    fn sync_data(self) throws (IoError);                 // fdatasync
    fn set_len(self, size: u64) throws (IoError);        // truncate

    fn metadata(self) throws (IoError) -> Metadata;
    fn permissions(self) throws (IoError) -> Permissions;
    fn set_permissions(self, perm: Permissions) throws (IoError);

    fn seek(self, pos: SeekFrom) throws (IoError) -> u64;
    fn try_clone(self) throws (IoError) -> File;         // dup fd

    // 파일 잠금 (Rust 1.77 stabilized, 2024)
    fn lock_exclusive(self) throws (IoError);            // flock LOCK_EX / LockFileEx
    fn lock_shared(self) throws (IoError);               // flock LOCK_SH
    fn try_lock_exclusive(self) throws (IoError) -> bool;
    fn unlock(self) throws (IoError);

    // 메모리 매핑 (mmap)
    unsafe fn mmap(self, len: usize, offset: u64) throws (IoError) -> Mmap;
}

enum SeekFrom {
    Start(u64),
    End(i64),
    Current(i64),
}

File 은 2.13 marker 의 Drop 을 구현하므로 using 블록으로 묶으면 블록을 빠져나갈 때 close 됩니다. using 없이 연 파일은 자동으로 닫히지 않으므로 close() 를 직접 부릅니다. OS handle (File, Dir, socket, mmap, 자식의 pidfd) 은 값의 복사본 몇 개가 닫든 한 번만 해제됩니다.

Mmap

unsafe fn File::mmap(len, offset) 는 offset (페이지 크기의 배수) 에서 len 바이트를 읽기 전용 공유 mapping 으로 매핑합니다. 파일의 다른 writer 가 mapping 아래의 바이트를 바꾸고, 파일을 자르면 읽기가 fault 나므로 unsafe 입니다.

struct Mmap { /* address, length */ }

impl Mmap {
    fn len(self) -> usize;                         // 0 after unmap
    fn is_empty(self) -> bool;
    fn get(self, i: usize) -> u8 | none;           // none past the end
    fn slice(self, start: usize, end: usize) -> List<u8>;   // copy; clamped to the mapping
    fn to_list(self) -> List<u8>;                  // copy of every byte
    fn unmap(self);
}

impl Drop for Mmap { ... }                         // unmaps

mapping 은 unmap 이나 drop (using 을 빠져나갈 때) 에서 해제되며, 그 뒤의 읽기는 빈 mapping 을 봅니다.

3.1.4. 편의 함수 (Dir::root() 위)

매번 Dir 핸들을 들고 다니지 않아도 되도록, Dir::root() 위에서 동작하는 편의 함수가 마련됩니다. 단순한 읽기와 쓰기, 메타데이터 조회, 존재 여부 확인 같은 일상적인 작업을 한 줄로 처리할 수 있고, atomic 쓰기와 재귀 순회처럼 자주 필요한 패턴도 같은 위치에 함께 노출됩니다.

fn read(path: Path) throws (IoError) -> List<u8>;
fn read_to_str(path: Path) throws (IoError) -> str;
fn write<B: AsBytes>(path: Path, data: B) throws (IoError);
fn write_atomic<B: AsBytes>(path: Path, data: B) throws (IoError);
                                          // tempfile + fsync + rename + fsync(parent)

fn metadata(path: Path) throws (IoError) -> Metadata;
fn exists(path: Path) -> bool;
fn create_dir(path: Path) throws (IoError);
fn create_dir_all(path: Path) throws (IoError);
fn remove_file(path: Path) throws (IoError);
fn remove_dir_all(path: Path) throws (IoError);
fn rename(from: Path, to: Path) throws (IoError);
fn copy(from: Path, to: Path) throws (IoError) -> u64;

fn walk(root: Path) -> Walk;             // Iterator<Item = Result<DirEntry, IoError>> 구현

Metadata

impl Metadata {
    fn len(self) -> u64;
    fn is_file(self) -> bool;
    fn is_dir(self) -> bool;
    fn is_symlink(self) -> bool;
    fn permissions(self) -> Permissions;
    fn modified(self) throws (IoError) -> SystemTime;    // 마지막 수정 시각
    fn accessed(self) throws (IoError) -> SystemTime;    // 마지막 접근 시각
    fn mtime(self) -> i64;                // Unix 초 (원시 값)
    fn atime(self) -> i64;
    fn ctime(self) -> i64;
}

시각은 SystemTime (3.3.3) 으로 얻습니다. mtime / atime / ctime 은 OS 의 Unix 초를 그대로 돌려줍니다.

walk

walk 는 깊이 우선으로 순회합니다. root 자신을 먼저 내고, 각 디렉토리 안의 항목은 이름 순으로 정렬해 냅니다. 원소는 Result<DirEntry, IoError> 이며, 읽을 수 없는 항목은 Err 로 나오고 순회는 계속됩니다.

write_atomic - POSIX 표준 패턴

write_atomic 은 임시 파일을 만들고 데이터를 기록한 뒤 fsync 와 rename, 그리고 부모 디렉토리의 fsync 까지 한 번에 처리하는 표준 패턴을 캡슐화합니다.

try fs::write_atomic("config.toml", new_config_bytes);
// 내부 동작.
//   1. tempfile create (.config.toml.tmp.<uuid>)
//   2. write data
//   3. fsync(temp)
//   4. rename(temp, config.toml)   ← atomic
//   5. fsync(parent dir)

이 패턴은 robust 한 앱이라면 어디에서나 필요하기에, Rust 에서는 tempfile crate, Go 에서는 renameio 가 std 의 빈자리를 채워 왔습니다. rok 은 같은 기능을 std 안에서 곧바로 제공합니다.

3.1.4a. 비동기 파일 I/O (*_async)

위의 함수와 메서드는 호출한 OS 스레드를 막습니다. task 안에서 기다리는 동안 worker 를 내주려면 같은 연산의 *_async 판을 await 합니다 (3.2.0 net 과 같은 이름 규칙). 보통 함수는 stackless 코루틴을 중단시킬 수 없으므로, sync 호출이 task 안에서 저절로 async 가 되지는 않습니다.

// 편의 함수 - 모두 async, exists_async 외에는 throws (IoError)
async fn open_async(path: Path) throws (IoError) -> File;
async fn create_async(path: Path) throws (IoError) -> File;
async fn open_with_async(path: Path, opts: OpenOptions) throws (IoError) -> File;
async fn read_async(path: Path) throws (IoError) -> List<u8>;
async fn read_to_str_async(path: Path) throws (IoError) -> str;
async fn write_async<B: AsBytes>(path: Path, data: B) throws (IoError);
async fn write_atomic_async<B: AsBytes>(path: Path, data: B) throws (IoError);
async fn metadata_async(path: Path) throws (IoError) -> Metadata;
async fn symlink_metadata_async(path: Path) throws (IoError) -> Metadata;
async fn exists_async(path: Path) -> bool;
async fn create_dir_async(path: Path) throws (IoError);
async fn create_dir_all_async(path: Path) throws (IoError);
async fn remove_file_async(path: Path) throws (IoError);
async fn remove_dir_async(path: Path) throws (IoError);
async fn remove_dir_all_async(path: Path) throws (IoError);
async fn rename_async(from: Path, to: Path) throws (IoError);
async fn copy_async(from: Path, to: Path) throws (IoError) -> u64;
async fn symlink_async(target: Path, link: Path) throws (IoError);
async fn hardlink_async(target: Path, link: Path) throws (IoError);
async fn read_dir_async(path: Path) throws (IoError) -> List<DirEntry>;
                                          // 한 번에 모두 읽음 (eager), 항목 에러가 있으면 첫 에러를 throw

impl File {
    async fn open_async(path: Path) throws (IoError) -> File;
    async fn create_async(path: Path) throws (IoError) -> File;
    async fn read_async(self, buf: List<u8>) throws (IoError) -> usize;
    async fn read_to_end_async(self) throws (IoError) -> List<u8>;
    async fn read_to_str_async(self) throws (IoError) -> str;
    async fn write_async<B: AsBytes>(self, data: B) throws (IoError) -> usize;
    async fn write_all_async<B: AsBytes>(self, data: B) throws (IoError);
    async fn write_str_async(self, s: str) throws (IoError);
    async fn sync_all_async(self) throws (IoError);
    async fn sync_data_async(self) throws (IoError);
    async fn set_len_async(self, size: u64) throws (IoError);
    async fn metadata_async(self) throws (IoError) -> Metadata;
    async fn lock_exclusive_async(self) throws (IoError);
    async fn lock_shared_async(self) throws (IoError);
}

Dir 도 모든 sync 메서드의 *_async 판을 가집니다.

impl Dir {
    async fn cwd_async() throws (IoError) -> Dir;
    async fn home_async() throws (IoError) -> Dir;
    async fn temp_async() throws (IoError) -> Dir;
    async fn open_async(self, path: Path) throws (IoError) -> File;
    async fn create_async(self, path: Path) throws (IoError) -> File;
    async fn open_with_async(self, path: Path, opts: OpenOptions) throws (IoError) -> File;
    async fn open_dir_async(self, path: Path) throws (IoError) -> Dir;
    async fn read_dir_async(self) throws (IoError) -> ReadDirStream;
    async fn metadata_async(self, path: Path) throws (IoError) -> Metadata;
    async fn symlink_metadata_async(self, path: Path) throws (IoError) -> Metadata;
    async fn exists_async(self, path: Path) -> bool;
    async fn create_dir_async(self, path: Path) throws (IoError);
    async fn create_dir_all_async(self, path: Path) throws (IoError);
    async fn remove_file_async(self, path: Path) throws (IoError);
    async fn remove_dir_async(self, path: Path) throws (IoError);
    async fn remove_dir_all_async(self, path: Path) throws (IoError);
    async fn rename_async(self, from: Path, to: Path) throws (IoError);
    async fn copy_async(self, from: Path, to: Path) throws (IoError) -> u64;
    async fn symlink_async(self, target: Path, link: Path) throws (IoError);
    async fn hardlink_async(self, target: Path, link: Path) throws (IoError);
    async fn close_async(self) throws (IoError);
}

struct ReadDirStream { /* 디렉토리 핸들 + 읽어 둔 batch */ }
impl Stream for ReadDirStream { type Item = Result<DirEntry, IoError>; ... }
impl Drop for ReadDirStream { ... }        // 닫음

read_dir_async 의 ReadDirStream 은 항목을 64 개씩 blocking pool 에서 읽어 for await 로 내주는 stream 입니다. 끝까지 읽거나 using 을 벗어나면 디렉토리를 닫습니다. 한 번에 모두 읽는 fs::read_dir_async(path) (List<DirEntry>) 와 달리 큰 디렉토리도 조금씩 읽습니다.

let logs = try await Dir::root().open_dir_async("logs");
using entries = try await logs.read_dir_async() {
    for await e in entries {
        let entry = try e.get();
        ...
    }
}

덧붙여 쓰는 전용 함수는 없습니다. open_with_async(path, OpenOptions::new().append(true)) 로 엽니다.

동작. 일반 파일에는 readiness 가 없으므로 (kqueue / epoll 에서 늘 준비됨), 각 *_async 호출은 같은 이름의 blocking 호출을 다른 OS 스레드에서 실행하고 (task::spawn_blocking), 그동안 task 를 중단합니다. 결과와 에러는 sync 호출과 똑같습니다.

취소. 기다리던 task 가 취소되어도 (2.18.4) 이미 시작된 호출은 백그라운드에서 끝까지 실행되고 그 결과만 버려집니다. 따라서 쓰기, 삭제, 이름 바꾸기 같은 부수 효과는 일어납니다.

async fn load(path: str) throws (IoError) -> Config {
    let text = try await fs::read_to_str_async(path);
    return try parse(text);
}

3.1.5. 사용 예

다음 예제는 단순 읽기에서 atomic 쓰기, sandbox 구성, 재귀 순회, 그리고 동시 프로세스를 위한 파일 잠금까지 일반적인 흐름을 차례로 보여 줍니다.

use rok::std::fs::*;
use rok::std::path::Path;

// 단순 읽기
let bytes = try fs::read("data.txt");
let text = try fs::read_to_str("config.toml");

// 안전한 쓰기 (atomic)
try fs::write_atomic("output.json", json_bytes);

// Dir capability - sandbox
let data_dir = try Dir::root().open_dir("data");
let f = try data_dir.open("a.txt");     // data/a.txt
let g = try data_dir.open("../etc/passwd");   // IoError - .. 거부 (symlink 는 막지 못함)

// 재귀 순회
for entry in fs::walk(Path::from("src")) {
    let entry = try entry.get();         // 원소는 Result - get() 으로 에러 흐름 복귀
    if entry.path().extension() == "rok" {
        process(entry.path());
    }
}

// File 잠금 (concurrent process)
using f = try fs::open_with(path, OpenOptions::default().write(true)) {
    try f.lock_exclusive();
    // critical section
}     // ← unlock + close 자동

3.1.6. 모듈 매핑 / target 가용성

각 기능이 어떤 target 에서 어떻게 동작하는지는 다음 표와 같습니다.

항목linux/macos/windowswasi비고
Dir/File 기본✓✓WASI 는 cap 기반 자연스러움
write_atomic✓✓항상 가능
lock_exclusive✓ (flock/LockFileEx)✗wasm32 에서 Unsupported
try_clone (dup)✓✗wasm32 에서 Unsupported
mmap✓✗wasm32 에서 Unsupported
copy reflink✓ (btrfs/APFS/XFS)✗자동 fallback to copy
pidfd / pipe2linux only✗feature-gated
walk✓✓

브라우저 (wasm32-unknown-unknown) 에서는 모든 fs 호출이 Unsupported 를 보고합니다: 함께 제공되는 JS host 는 / 를 pre-open 하고 모든 호출을 거절합니다.

GitHub에서 이 페이지 고치기