Skip to main content

async_fs_io/
file.rs

1//! Bounded file convenience operations.
2
3use std::path::{
4    Path,
5    PathBuf,
6};
7
8use tokio::io::AsyncReadExt;
9
10use crate::{
11    AsyncFile,
12    FsError,
13    Operation,
14};
15
16/// Metadata needed without exposing filesystem-specific read helpers.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct FileMetadata {
19    /// File length in bytes.
20    pub length: u64,
21    /// Whether the path identifies a directory.
22    pub is_directory: bool,
23}
24
25/// Canonicalize a path through the asynchronous filesystem boundary.
26pub async fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, FsError> {
27    let path = path.as_ref();
28    tokio::fs::canonicalize(path)
29        .await
30        .map_err(|error| FsError::io(Operation::Read, path, error))
31}
32
33/// Read a file only when its complete contents fit under `max_bytes`.
34///
35/// The implementation reads at most `max_bytes + 1` bytes and rejects a file
36/// that exceeds the declared bound, so a stale metadata length cannot turn
37/// this convenience API into an unbounded allocation.
38pub async fn read_bounded(path: impl AsRef<Path>, max_bytes: usize) -> Result<Vec<u8>, FsError> {
39    let path = path.as_ref();
40    let file = AsyncFile::open(path).await?;
41    let read_limit = max_bytes
42        .checked_add(1)
43        .ok_or_else(|| FsError::InvalidRequest("read bound overflow".to_owned()))?;
44    // bounded: this allocation is capped by the caller-provided read limit.
45    let mut output = Vec::with_capacity(max_bytes.min(64 * 1024));
46    file.take(read_limit as u64)
47        .read_to_end(&mut output)
48        .await
49        .map_err(|error| FsError::io(Operation::Read, path, error))?;
50    if output.len() > max_bytes {
51        return Err(FsError::InvalidRequest(format!(
52            "file {} exceeds the {} byte read bound",
53            path.display(),
54            max_bytes
55        )));
56    }
57    Ok(output)
58}
59
60/// Read UTF-8 text only when its complete contents fit under `max_bytes`.
61pub async fn read_string_bounded(
62    path: impl AsRef<Path>,
63    max_bytes: usize,
64) -> Result<String, FsError> {
65    let path = path.as_ref();
66    let bytes = read_bounded(path, max_bytes).await?;
67    String::from_utf8(bytes).map_err(|error| {
68        FsError::io(
69            Operation::Read,
70            path,
71            format!("file is not valid UTF-8: {error}"),
72        )
73    })
74}
75
76/// Read UTF-8 text only when its complete contents fit under `max_bytes`,
77/// returning `None` when the file does not exist.
78pub async fn read_string_bounded_if_exists(
79    path: impl AsRef<Path>,
80    max_bytes: usize,
81) -> Result<Option<String>, FsError> {
82    let path = path.as_ref();
83    let Some(file) = AsyncFile::open_if_exists(path).await? else {
84        return Ok(None);
85    };
86    let read_limit = max_bytes
87        .checked_add(1)
88        .ok_or_else(|| FsError::InvalidRequest("read bound overflow".to_owned()))?;
89    // bounded: this allocation is capped by the caller-provided read limit.
90    let mut output = Vec::with_capacity(max_bytes.min(64 * 1024));
91    file.take(read_limit as u64)
92        .read_to_end(&mut output)
93        .await
94        .map_err(|error| FsError::io(Operation::Read, path, error))?;
95    if output.len() > max_bytes {
96        return Err(FsError::InvalidRequest(format!(
97            "file {} exceeds the {} byte read bound",
98            path.display(),
99            max_bytes
100        )));
101    }
102    String::from_utf8(output)
103        .map(Some)
104        .map_err(|error| FsError::io(Operation::Read, path, error))
105}
106
107/// Return metadata through the async filesystem boundary.
108pub async fn metadata(path: impl AsRef<Path>) -> Result<FileMetadata, FsError> {
109    let path = path.as_ref();
110    let metadata = tokio::fs::metadata(path)
111        .await
112        .map_err(|error| FsError::io(Operation::Read, path, error))?;
113    Ok(FileMetadata {
114        length: metadata.len(),
115        is_directory: metadata.is_dir(),
116    })
117}
118
119/// Return operating-system metadata without following a symbolic link.
120pub async fn symlink_metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata, FsError> {
121    let path = path.as_ref();
122    tokio::fs::symlink_metadata(path)
123        .await
124        .map_err(|error| FsError::io(Operation::Read, path, error))
125}
126
127/// Set filesystem permissions through the asynchronous filesystem boundary.
128pub async fn set_permissions(
129    path: impl AsRef<Path>,
130    permissions: std::fs::Permissions,
131) -> Result<(), FsError> {
132    let path = path.as_ref();
133    tokio::fs::set_permissions(path, permissions)
134        .await
135        .map_err(|error| FsError::io(Operation::Write, path, error))
136}
137
138/// Return whether a path exists, preserving errors other than not-found.
139pub async fn try_exists(path: impl AsRef<Path>) -> Result<bool, FsError> {
140    let path = path.as_ref();
141    match tokio::fs::metadata(path).await {
142        Ok(_) => Ok(true),
143        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
144        Err(error) => Err(FsError::io(Operation::Read, path, error)),
145    }
146}
147
148/// Write a bounded caller-owned byte slice to a file, replacing its contents.
149pub async fn write_bytes(path: impl AsRef<Path>, bytes: &[u8]) -> Result<(), FsError> {
150    let path = path.as_ref();
151    let mut file = AsyncFile::create(path).await?;
152    file.write_all(bytes).await?;
153    file.flush().await
154}
155
156/// Remove a file if it exists.
157pub async fn remove_if_exists(path: impl AsRef<Path>) -> Result<(), FsError> {
158    let path = path.as_ref();
159    match tokio::fs::remove_file(path).await {
160        Ok(()) => Ok(()),
161        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
162        Err(error) => Err(FsError::io(Operation::Write, path, error)),
163    }
164}
165
166/// Remove a file, reporting a missing file as an error.
167pub async fn remove_file(path: impl AsRef<Path>) -> Result<(), FsError> {
168    let path = path.as_ref();
169    tokio::fs::remove_file(path)
170        .await
171        .map_err(|error| FsError::io(Operation::Write, path, error))
172}
173
174/// Rename a file or directory.
175pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<(), FsError> {
176    let from = from.as_ref();
177    let to = to.as_ref();
178    tokio::fs::rename(from, to).await.map_err(|error| {
179        FsError::io(
180            Operation::Write,
181            to,
182            format!("from {}: {error}", from.display()),
183        )
184    })
185}