uring_file/
fs.rs

1//! High-level filesystem operations using io_uring.
2//!
3//! This module provides convenience functions that mirror `std::fs`, but use io_uring for async I/O.
4//! All functions use the global default ring.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use uring_file::fs::{self, OpenOptions};
10//!
11//! #[tokio::main]
12//! async fn main() -> std::io::Result<()> {
13//!     // Read entire file
14//!     let contents = fs::read_to_string("/etc/hostname").await?;
15//!     println!("Hostname: {}", contents.trim());
16//!
17//!     // Write to file
18//!     fs::write("/tmp/hello.txt", b"Hello, world!").await?;
19//!
20//!     // Open with options
21//!     let file = OpenOptions::new()
22//!         .read(true)
23//!         .write(true)
24//!         .create(true)
25//!         .open("/tmp/data.bin")
26//!         .await?;
27//!
28//!     // Create directory tree
29//!     fs::create_dir_all("/tmp/a/b/c").await?;
30//!
31//!     // Remove directory tree
32//!     fs::remove_dir_all("/tmp/a").await?;
33//!
34//!     Ok(())
35//! }
36//! ```
37
38use crate::default_uring;
39use crate::metadata::Metadata;
40use crate::uring::URING_LEN_MAX;
41use std::cmp::min;
42use std::io;
43use std::path::Path;
44use tokio::fs::File;
45
46/// Options for opening files, mirroring `std::fs::OpenOptions`.
47///
48/// # Example
49///
50/// ```ignore
51/// use uring_file::fs::OpenOptions;
52///
53/// // Open for reading
54/// let file = OpenOptions::new().read(true).open("foo.txt").await?;
55///
56/// // Create for writing
57/// let file = OpenOptions::new()
58///     .write(true)
59///     .create(true)
60///     .truncate(true)
61///     .open("bar.txt")
62///     .await?;
63///
64/// // Append to existing
65/// let file = OpenOptions::new()
66///     .append(true)
67///     .open("log.txt")
68///     .await?;
69///
70/// // Create new (fails if exists)
71/// let file = OpenOptions::new()
72///     .write(true)
73///     .create_new(true)
74///     .open("new.txt")
75///     .await?;
76/// ```
77#[derive(Clone, Debug, Default)]
78pub struct OpenOptions {
79  read: bool,
80  write: bool,
81  append: bool,
82  truncate: bool,
83  create: bool,
84  create_new: bool,
85  mode: Option<u32>,
86  custom_flags: i32,
87}
88
89impl OpenOptions {
90  /// Creates a blank new set of options.
91  ///
92  /// All options are initially set to `false`.
93  pub fn new() -> Self {
94    Self::default()
95  }
96
97  /// Sets read access.
98  pub fn read(&mut self, read: bool) -> &mut Self {
99    self.read = read;
100    self
101  }
102
103  /// Sets write access.
104  pub fn write(&mut self, write: bool) -> &mut Self {
105    self.write = write;
106    self
107  }
108
109  /// Sets append mode.
110  ///
111  /// Writes will append to the file instead of overwriting.
112  /// Implies `write(true)`.
113  pub fn append(&mut self, append: bool) -> &mut Self {
114    self.append = append;
115    self
116  }
117
118  /// Sets truncate mode.
119  ///
120  /// If the file exists, it will be truncated to zero length.
121  /// Requires `write(true)`.
122  pub fn truncate(&mut self, truncate: bool) -> &mut Self {
123    self.truncate = truncate;
124    self
125  }
126
127  /// Sets create mode.
128  ///
129  /// Creates the file if it doesn't exist. Requires `write(true)` or `append(true)`.
130  pub fn create(&mut self, create: bool) -> &mut Self {
131    self.create = create;
132    self
133  }
134
135  /// Sets create-new mode.
136  ///
137  /// Creates a new file, failing if it already exists.
138  /// Implies `create(true)` and requires `write(true)`.
139  pub fn create_new(&mut self, create_new: bool) -> &mut Self {
140    self.create_new = create_new;
141    self
142  }
143
144  /// Sets the file mode (permissions) for newly created files.
145  ///
146  /// Default is `0o644`.
147  pub fn mode(&mut self, mode: u32) -> &mut Self {
148    self.mode = Some(mode);
149    self
150  }
151
152  /// Sets custom flags to pass to the underlying `open` syscall.
153  ///
154  /// This allows flags like `O_DIRECT`, `O_SYNC`, `O_NOFOLLOW`, `O_CLOEXEC`, etc.
155  /// The flags are OR'd with the flags derived from other options.
156  ///
157  /// # Example
158  ///
159  /// ```ignore
160  /// let file = OpenOptions::new()
161  ///     .read(true)
162  ///     .write(true)
163  ///     .create(true)
164  ///     .custom_flags(libc::O_DIRECT | libc::O_SYNC)
165  ///     .open("data.bin")
166  ///     .await?;
167  /// ```
168  pub fn custom_flags(&mut self, flags: i32) -> &mut Self {
169    self.custom_flags = flags;
170    self
171  }
172
173  /// Opens a file with the configured options.
174  ///
175  /// Returns a `tokio::fs::File` for async operations.
176  pub async fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
177    let flags = self.to_flags()?;
178    let mode = self.mode.unwrap_or(0o644);
179    let fd = default_uring().open(path, flags, mode).await?;
180    Ok(File::from_std(std::fs::File::from(fd)))
181  }
182
183  fn to_flags(&self) -> io::Result<i32> {
184    // Validate options (matches std::fs::OpenOptions behavior)
185    if self.append && self.truncate {
186      return Err(io::Error::new(
187        io::ErrorKind::InvalidInput,
188        "cannot combine append and truncate",
189      ));
190    }
191
192    // Build flags
193    let mut flags = self.custom_flags;
194
195    // Access mode: determine O_RDONLY, O_WRONLY, or O_RDWR
196    let has_write = self.write || self.append;
197    if self.read && has_write {
198      flags |= libc::O_RDWR;
199    } else if self.read {
200      flags |= libc::O_RDONLY;
201    } else if has_write {
202      flags |= libc::O_WRONLY;
203    } else {
204      // Match std behavior: default to read-only if nothing specified
205      flags |= libc::O_RDONLY;
206    }
207
208    if self.append {
209      flags |= libc::O_APPEND;
210    }
211
212    if self.truncate {
213      flags |= libc::O_TRUNC;
214    }
215
216    if self.create_new {
217      flags |= libc::O_CREAT | libc::O_EXCL;
218    } else if self.create {
219      flags |= libc::O_CREAT;
220    }
221
222    Ok(flags)
223  }
224}
225
226/// Open a file in read-only mode.
227///
228/// Returns a `tokio::fs::File` which can be used with the `UringFile` trait or tokio's async methods.
229pub async fn open(path: impl AsRef<Path>) -> io::Result<File> {
230  let fd = default_uring().open(path, libc::O_RDONLY, 0).await?;
231  Ok(File::from_std(std::fs::File::from(fd)))
232}
233
234/// Create a file for writing, truncating if it exists.
235///
236/// Returns a `tokio::fs::File` which can be used with the `UringFile` trait or tokio's async methods.
237pub async fn create(path: impl AsRef<Path>) -> io::Result<File> {
238  let fd = default_uring()
239    .open(path, libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC, 0o644)
240    .await?;
241  Ok(File::from_std(std::fs::File::from(fd)))
242}
243
244/// Read the entire contents of a file into a byte vector.
245///
246/// Handles files larger than io_uring's single-operation limit (~2GB) by chunking automatically.
247pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
248  let uring = default_uring();
249  let fd = uring.open(path.as_ref(), libc::O_RDONLY, 0).await?;
250  let meta = uring.statx(&fd).await?;
251  let size = meta.len();
252
253  if size == 0 {
254    return Ok(Vec::new());
255  }
256
257  // For small files, read in one operation
258  if size <= URING_LEN_MAX {
259    return uring.read_exact_at(&fd, 0, size).await;
260  }
261
262  // For large files, read in chunks
263  let mut buf = Vec::with_capacity(size as usize);
264  let mut offset = 0u64;
265  while offset < size {
266    let chunk_size = min(URING_LEN_MAX, size - offset);
267    let chunk = uring.read_exact_at(&fd, offset, chunk_size).await?;
268    buf.extend_from_slice(&chunk);
269    offset += chunk_size;
270  }
271  Ok(buf)
272}
273
274/// Read the entire contents of a file into a string.
275///
276/// Returns an error if the file contents are not valid UTF-8.
277///
278/// Unlike `std::fs::read_to_string`, this does not handle `io::ErrorKind::Interrupted` because io_uring operations are not interrupted by signals — the kernel completes them asynchronously.
279pub async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
280  let bytes = read(path).await?;
281  String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
282}
283
284/// Write data to a file, creating it if it doesn't exist, truncating it if it does.
285pub async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
286  let uring = default_uring();
287  let fd = uring
288    .open(
289      path.as_ref(),
290      libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
291      0o644,
292    )
293    .await?;
294  uring.write_all_at(&fd, 0, contents.as_ref()).await
295}
296
297/// Get metadata for a file or directory.
298pub async fn metadata(path: impl AsRef<Path>) -> io::Result<Metadata> {
299  default_uring().statx_path(path).await
300}
301
302/// Create a directory.
303///
304/// Returns an error if the directory already exists or the parent doesn't exist.
305pub async fn create_dir(path: impl AsRef<Path>) -> io::Result<()> {
306  default_uring().mkdir(path, 0o755).await
307}
308
309/// Create a directory and all parent directories.
310///
311/// Does nothing if the directory already exists.
312pub async fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
313  let path = path.as_ref();
314
315  // If it already exists, we're done
316  if metadata(path).await.is_ok() {
317    return Ok(());
318  }
319
320  // Collect ancestors that need to be created
321  let mut to_create = Vec::new();
322  let mut current = Some(path);
323
324  while let Some(p) = current {
325    if metadata(p).await.is_ok() {
326      break;
327    }
328    to_create.push(p);
329    current = p.parent();
330  }
331
332  // Create from root to leaf
333  for p in to_create.into_iter().rev() {
334    match default_uring().mkdir(p, 0o755).await {
335      Ok(()) => {}
336      Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
337      Err(e) => return Err(e),
338    }
339  }
340
341  Ok(())
342}
343
344/// Remove a file.
345pub async fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
346  default_uring().unlink(path).await
347}
348
349/// Remove an empty directory.
350pub async fn remove_dir(path: impl AsRef<Path>) -> io::Result<()> {
351  default_uring().rmdir(path).await
352}
353
354/// Remove a directory and all its contents recursively.
355///
356/// Uses tokio for directory listing (io_uring doesn't support readdir), then io_uring for removal.
357pub async fn remove_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
358  let path = path.as_ref();
359
360  // io_uring doesn't have readdir, use tokio's async version
361  let mut read_dir = tokio::fs::read_dir(path).await?;
362  while let Some(entry) = read_dir.next_entry().await? {
363    let entry_path = entry.path();
364    let file_type = entry.file_type().await?;
365    if file_type.is_dir() {
366      Box::pin(remove_dir_all(&entry_path)).await?;
367    } else {
368      remove_file(&entry_path).await?;
369    }
370  }
371
372  remove_dir(path).await
373}
374
375/// Rename a file or directory.
376pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
377  default_uring().rename(from, to).await
378}
379
380/// Copy the contents of one file to another.
381///
382/// Creates the destination file if it doesn't exist, truncates it if it does.
383pub async fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
384  let contents = read(from).await?;
385  let len = contents.len() as u64;
386  write(to, contents).await?;
387  Ok(len)
388}
389
390/// Check if a path exists.
391pub async fn exists(path: impl AsRef<Path>) -> bool {
392  metadata(path).await.is_ok()
393}
394
395/// Create a symbolic link.
396///
397/// `target` is what the symlink points to, `link` is the path of the new symlink.
398pub async fn symlink(target: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
399  default_uring().symlink(target, link).await
400}
401
402/// Create a hard link.
403///
404/// Creates a new hard link `link` pointing to the same inode as `original`.
405pub async fn hard_link(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
406  default_uring().hard_link(original, link).await
407}
408
409/// Truncate a file to the specified length.
410///
411/// If the file is larger, it will be truncated. If smaller, it will be extended with zeros.
412pub async fn truncate(path: impl AsRef<Path>, len: u64) -> io::Result<()> {
413  let uring = default_uring();
414  let fd = uring.open(path.as_ref(), libc::O_WRONLY, 0).await?;
415  uring.ftruncate(&fd, len).await
416}
417
418/// Append data to a file, creating it if it doesn't exist.
419pub async fn append(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
420  let uring = default_uring();
421  let fd = uring
422    .open(
423      path.as_ref(),
424      libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND,
425      0o644,
426    )
427    .await?;
428  // With O_APPEND, the offset is ignored - kernel always writes at end
429  uring.write_all_at(&fd, 0, contents.as_ref()).await
430}