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;
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//!     // Create directory tree
21//!     fs::create_dir_all("/tmp/a/b/c").await?;
22//!
23//!     // Remove directory tree
24//!     fs::remove_dir_all("/tmp/a").await?;
25//!
26//!     Ok(())
27//! }
28//! ```
29
30use crate::default_uring;
31use crate::metadata::Metadata;
32use std::io;
33use std::path::Path;
34use tokio::fs::File;
35
36/// Open a file in read-only mode.
37///
38/// Returns a `tokio::fs::File` which can be used with the `UringFile` trait or tokio's async methods.
39pub async fn open(path: impl AsRef<Path>) -> io::Result<File> {
40  let fd = default_uring().open(path, libc::O_RDONLY, 0).await?;
41  Ok(File::from_std(std::fs::File::from(fd)))
42}
43
44/// Create a file for writing, truncating if it exists.
45///
46/// Returns a `tokio::fs::File` which can be used with the `UringFile` trait or tokio's async methods.
47pub async fn create(path: impl AsRef<Path>) -> io::Result<File> {
48  let fd = default_uring()
49    .open(path, libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC, 0o644)
50    .await?;
51  Ok(File::from_std(std::fs::File::from(fd)))
52}
53
54/// Read the entire contents of a file into a byte vector.
55///
56/// This is a convenience function for reading entire files. For large files, consider using `Uring::read_at` with explicit offsets and chunking.
57pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
58  let uring = default_uring();
59  let fd = uring.open(path.as_ref(), libc::O_RDONLY, 0).await?;
60  let meta = uring.statx(&fd).await?;
61  let size = meta.len();
62
63  if size == 0 {
64    return Ok(Vec::new());
65  }
66
67  let result = uring.read_at(&fd, 0, size).await?;
68  Ok(result.buf)
69}
70
71/// Read the entire contents of a file into a string.
72///
73/// Returns an error if the file contents are not valid UTF-8.
74pub async fn read_to_string(path: impl AsRef<Path>) -> io::Result<String> {
75  let bytes = read(path).await?;
76  String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
77}
78
79/// Write data to a file, creating it if it doesn't exist, truncating it if it does.
80pub async fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
81  let uring = default_uring();
82  let fd = uring
83    .open(
84      path.as_ref(),
85      libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC,
86      0o644,
87    )
88    .await?;
89  let data = contents.as_ref().to_vec();
90  uring.write_at(&fd, 0, data).await?;
91  Ok(())
92}
93
94/// Get metadata for a file or directory.
95pub async fn metadata(path: impl AsRef<Path>) -> io::Result<Metadata> {
96  default_uring().statx_path(path).await
97}
98
99/// Create a directory.
100///
101/// Returns an error if the directory already exists or the parent doesn't exist.
102pub async fn create_dir(path: impl AsRef<Path>) -> io::Result<()> {
103  default_uring().mkdir(path, 0o755).await
104}
105
106/// Create a directory and all parent directories.
107///
108/// Does nothing if the directory already exists.
109pub async fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
110  let path = path.as_ref();
111
112  // If it already exists, we're done
113  if metadata(path).await.is_ok() {
114    return Ok(());
115  }
116
117  // Collect ancestors that need to be created
118  let mut to_create = Vec::new();
119  let mut current = Some(path);
120
121  while let Some(p) = current {
122    if metadata(p).await.is_ok() {
123      break;
124    }
125    to_create.push(p);
126    current = p.parent();
127  }
128
129  // Create from root to leaf
130  for p in to_create.into_iter().rev() {
131    match default_uring().mkdir(p, 0o755).await {
132      Ok(()) => {}
133      Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
134      Err(e) => return Err(e),
135    }
136  }
137
138  Ok(())
139}
140
141/// Remove a file.
142pub async fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
143  default_uring().unlink(path).await
144}
145
146/// Remove an empty directory.
147pub async fn remove_dir(path: impl AsRef<Path>) -> io::Result<()> {
148  default_uring().rmdir(path).await
149}
150
151/// Remove a directory and all its contents recursively.
152///
153/// Uses tokio for directory listing (io_uring doesn't support readdir), then io_uring for removal.
154pub async fn remove_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
155  let path = path.as_ref();
156
157  // io_uring doesn't have readdir, use tokio's async version
158  let mut read_dir = tokio::fs::read_dir(path).await?;
159  while let Some(entry) = read_dir.next_entry().await? {
160    let entry_path = entry.path();
161    let file_type = entry.file_type().await?;
162    if file_type.is_dir() {
163      Box::pin(remove_dir_all(&entry_path)).await?;
164    } else {
165      remove_file(&entry_path).await?;
166    }
167  }
168
169  remove_dir(path).await
170}
171
172/// Rename a file or directory.
173pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
174  default_uring().rename(from, to).await
175}
176
177/// Copy the contents of one file to another.
178///
179/// Creates the destination file if it doesn't exist, truncates it if it does.
180pub async fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<u64> {
181  let contents = read(from).await?;
182  let len = contents.len() as u64;
183  write(to, contents).await?;
184  Ok(len)
185}
186
187/// Check if a path exists.
188pub async fn exists(path: impl AsRef<Path>) -> bool {
189  metadata(path).await.is_ok()
190}
191
192/// Create a symbolic link.
193///
194/// `target` is what the symlink points to, `link` is the path of the new symlink.
195pub async fn symlink(target: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
196  default_uring().symlink(target, link).await
197}
198
199/// Create a hard link.
200///
201/// Creates a new hard link `link` pointing to the same inode as `original`.
202pub async fn hard_link(original: impl AsRef<Path>, link: impl AsRef<Path>) -> io::Result<()> {
203  default_uring().hard_link(original, link).await
204}
205
206/// Truncate a file to the specified length.
207///
208/// If the file is larger, it will be truncated. If smaller, it will be extended with zeros.
209pub async fn truncate(path: impl AsRef<Path>, len: u64) -> io::Result<()> {
210  let uring = default_uring();
211  let fd = uring.open(path.as_ref(), libc::O_WRONLY, 0).await?;
212  uring.ftruncate(&fd, len).await
213}
214
215/// Append data to a file, creating it if it doesn't exist.
216pub async fn append(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> io::Result<()> {
217  let uring = default_uring();
218  let fd = uring
219    .open(
220      path.as_ref(),
221      libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND,
222      0o644,
223    )
224    .await?;
225  let data = contents.as_ref().to_vec();
226  // With O_APPEND, the offset is ignored - kernel always writes at end
227  uring.write_at(&fd, 0, data).await?;
228  Ok(())
229}