use std::{
future::Future,
path::{Path, PathBuf, StripPrefixError},
};
use bytes::Bytes;
use crate::{
model::LocalFileInfo,
proto::{FileSystem, calculate_local_unix_path},
};
#[derive(Debug, Clone)]
pub struct TokioLocalStorage {
base: PathBuf,
ignore_parts: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum TokioLocalStorageError {
#[error("an operation out of the base direction is requested, is this some escape attack?. dir: {0}")]
AccessOutOfBaseDirection(PathBuf),
#[error("FileName not within Root Directory, is this some escape attack?")]
StripPrefixError(#[from] StripPrefixError),
#[error("Io Error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid UTF8-Filename. this code requires filenames to match UTF8")]
InvalidUtf8Filename,
}
impl TokioLocalStorage {
pub fn new(base: PathBuf, ignore_parts: Vec<String>) -> Self { Self { base, ignore_parts } }
fn sub_path(&self, path: &str) -> Result<PathBuf, TokioLocalStorageError> {
let path = self.base.join(path);
if !path.starts_with(&self.base) {
return Err(TokioLocalStorageError::AccessOutOfBaseDirection(path));
}
Ok(path)
}
}
impl FileSystem for TokioLocalStorage {
type Error = TokioLocalStorageError;
type StorePrepare = tokio::fs::File;
fn all_files(&self) -> impl Future<Output = Result<Vec<LocalFileInfo>, Self::Error>> + Send {
let mut nextdirs = Vec::new();
let mut file_infos = Vec::new();
async move {
let mut root_dir = tokio::fs::read_dir(&self.base).await?;
while let Some(entry) = root_dir.next_entry().await? {
let path = entry.path();
if path.is_dir() {
nextdirs.push(path);
}
}
while let Some(next) = nextdirs.pop() {
let mut dir = tokio::fs::read_dir(&next).await?;
while let Some(entry) = dir.next_entry().await? {
let path = entry.path();
let relative_path = path.strip_prefix(&self.base)?;
if self.ignore_parts.iter().any(|ignore| relative_path.starts_with(ignore)) {
continue;
}
if path.is_dir() {
nextdirs.push(path);
} else {
parse_file_info(&self.base, path, &mut file_infos).await?;
}
}
}
Ok(file_infos)
}
}
fn prepare_store_file(&self, path: &str) -> impl Future<Output = Result<Self::StorePrepare, Self::Error>> + Send {
let path = self.sub_path(path);
async move {
let path = path?;
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
};
let file = tokio::fs::File::create(path).await?;
Ok(file)
}
}
#[expect(clippy::manual_async_fn)]
fn store_file(
&self,
mut prepared: Self::StorePrepare,
mut data: Bytes,
) -> impl Future<Output = Result<(), Self::Error>> + Send {
async move {
use tokio::io::AsyncWriteExt;
prepared.write_all_buf(&mut data).await?;
Ok(())
}
}
fn delete_file(&self, path: &str) -> impl Future<Output = Result<(), Self::Error>> + Send {
let path = self.sub_path(path);
async move {
let path = path?;
tokio::fs::remove_file(path).await?;
Ok(())
}
}
}
async fn parse_file_info(
root: &Path,
path: PathBuf,
file_infos: &mut Vec<LocalFileInfo>,
) -> Result<(), TokioLocalStorageError> {
let file_bytes = tokio::fs::read(&path).await?;
let crc32 = crc32fast::hash(&file_bytes);
let local_unix_path = calculate_local_unix_path(root, &path).ok_or(TokioLocalStorageError::InvalidUtf8Filename)?;
file_infos.push(LocalFileInfo { crc32, local_unix_path });
Ok(())
}