Skip to main content

async_fs_io/
file.rs

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