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 에서는
tempfilecrate, 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/windows | wasi | 비고 |
|---|---|---|---|
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 / pipe2 | linux only | ✗ | feature-gated |
walk | ✓ | ✓ |
브라우저 (wasm32-unknown-unknown) 에서는 모든 fs 호출이 Unsupported 를 보고합니다: 함께 제공되는 JS host 는 / 를 pre-open 하고 모든 호출을 거절합니다.