remozipsy 0.0.1

zip implementation independent structs and helpers
Documentation
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(())
}