use std::path::{
Path,
PathBuf,
};
use tokio::io::AsyncReadExt;
use crate::{
AsyncFile,
FsError,
Operation,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileMetadata {
pub length: u64,
pub is_directory: bool,
}
pub async fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, FsError> {
let path = path.as_ref();
tokio::fs::canonicalize(path)
.await
.map_err(|error| FsError::io(Operation::Read, path, error))
}
pub async fn read_bounded(path: impl AsRef<Path>, max_bytes: usize) -> Result<Vec<u8>, FsError> {
let path = path.as_ref();
let file = AsyncFile::open(path).await?;
let read_limit = max_bytes
.checked_add(1)
.ok_or_else(|| FsError::InvalidRequest("read bound overflow".to_owned()))?;
let mut output = Vec::with_capacity(max_bytes.min(64 * 1024));
file.take(read_limit as u64)
.read_to_end(&mut output)
.await
.map_err(|error| FsError::io(Operation::Read, path, error))?;
if output.len() > max_bytes {
return Err(FsError::InvalidRequest(format!(
"file {} exceeds the {} byte read bound",
path.display(),
max_bytes
)));
}
Ok(output)
}
pub async fn read_string_bounded(
path: impl AsRef<Path>,
max_bytes: usize,
) -> Result<String, FsError> {
let path = path.as_ref();
let bytes = read_bounded(path, max_bytes).await?;
String::from_utf8(bytes).map_err(|error| {
FsError::io(
Operation::Read,
path,
format!("file is not valid UTF-8: {error}"),
)
})
}
pub async fn read_string_bounded_if_exists(
path: impl AsRef<Path>,
max_bytes: usize,
) -> Result<Option<String>, FsError> {
let path = path.as_ref();
let Some(file) = AsyncFile::open_if_exists(path).await? else {
return Ok(None);
};
let read_limit = max_bytes
.checked_add(1)
.ok_or_else(|| FsError::InvalidRequest("read bound overflow".to_owned()))?;
let mut output = Vec::with_capacity(max_bytes.min(64 * 1024));
file.take(read_limit as u64)
.read_to_end(&mut output)
.await
.map_err(|error| FsError::io(Operation::Read, path, error))?;
if output.len() > max_bytes {
return Err(FsError::InvalidRequest(format!(
"file {} exceeds the {} byte read bound",
path.display(),
max_bytes
)));
}
String::from_utf8(output)
.map(Some)
.map_err(|error| FsError::io(Operation::Read, path, error))
}
pub async fn metadata(path: impl AsRef<Path>) -> Result<FileMetadata, FsError> {
let path = path.as_ref();
let metadata = tokio::fs::metadata(path)
.await
.map_err(|error| FsError::io(Operation::Read, path, error))?;
Ok(FileMetadata {
length: metadata.len(),
is_directory: metadata.is_dir(),
})
}
pub async fn symlink_metadata(path: impl AsRef<Path>) -> Result<std::fs::Metadata, FsError> {
let path = path.as_ref();
tokio::fs::symlink_metadata(path)
.await
.map_err(|error| FsError::io(Operation::Read, path, error))
}
pub async fn set_permissions(
path: impl AsRef<Path>,
permissions: std::fs::Permissions,
) -> Result<(), FsError> {
let path = path.as_ref();
tokio::fs::set_permissions(path, permissions)
.await
.map_err(|error| FsError::io(Operation::Write, path, error))
}
pub async fn try_exists(path: impl AsRef<Path>) -> Result<bool, FsError> {
let path = path.as_ref();
match tokio::fs::metadata(path).await {
Ok(_) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(FsError::io(Operation::Read, path, error)),
}
}
pub async fn write_bytes(path: impl AsRef<Path>, bytes: &[u8]) -> Result<(), FsError> {
let path = path.as_ref();
let mut file = AsyncFile::create(path).await?;
file.write_all(bytes).await?;
file.flush().await
}
pub async fn remove_if_exists(path: impl AsRef<Path>) -> Result<(), FsError> {
let path = path.as_ref();
match tokio::fs::remove_file(path).await {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(FsError::io(Operation::Write, path, error)),
}
}
pub async fn remove_file(path: impl AsRef<Path>) -> Result<(), FsError> {
let path = path.as_ref();
tokio::fs::remove_file(path)
.await
.map_err(|error| FsError::io(Operation::Write, path, error))
}
pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<(), FsError> {
let from = from.as_ref();
let to = to.as_ref();
tokio::fs::rename(from, to).await.map_err(|error| {
FsError::io(
Operation::Write,
to,
format!("from {}: {error}", from.display()),
)
})
}