remozipsy 0.2.0

Remote Zip Sync - sync remote zip to local fs
Documentation
use std::{collections::HashMap, future::Future, path::PathBuf, sync::Arc};

use bytes::Bytes;
use tokio::sync::Mutex;

use crate::{
    model::FileInfo,
    proto::FileSystem,
    tokio::local::{TokioLocalStorage, TokioLocalStorageError},
};

/// Cached version of [`crate::tokio::TokioLocalStorage`].
#[derive(Debug, Clone)]
pub struct TokioCachedLocalStorage {
    inner: TokioLocalStorage,
    cache: Arc<Mutex<HashMap<String, FileInfo>>>,
}

impl TokioCachedLocalStorage {
    pub fn new(base: PathBuf, ignore_parts: Vec<String>, cache: Vec<FileInfo>) -> Self {
        let internal = TokioLocalStorage::new(base, ignore_parts);
        let cache: HashMap<_, _> = cache.into_iter().map(|e| (e.local_unix_path.clone(), e)).collect();
        let cache = Arc::new(Mutex::new(cache));
        Self { inner: internal, cache }
    }

    /// Might return None if still being used. Extracts the cache content
    pub fn try_cache_content(&self) -> Option<HashMap<String, FileInfo>> {
        let lock = self.cache.try_lock().ok()?;
        Some(lock.clone())
    }
}

impl FileSystem for TokioCachedLocalStorage {
    type Error = TokioLocalStorageError;
    type StorePrepare = (tokio::fs::File, FileInfo);

    fn all_files(&mut self) -> impl Future<Output = Result<Vec<FileInfo>, Self::Error>> + Send {
        let cache = self.cache.clone();
        async move {
            let guard = cache.lock().await;
            let cache = guard.clone().into_values().collect();
            Ok(cache)
        }
    }

    #[expect(clippy::manual_async_fn)]
    fn prepare_store_file(
        &self,
        info: FileInfo,
    ) -> impl Future<Output = Result<Self::StorePrepare, Self::Error>> + Send {
        async move { Ok((self.inner.prepare_store_file(info.clone()).await?, info)) }
    }

    fn store_file(
        &self,
        prepared: Self::StorePrepare,
        data: Bytes,
    ) -> impl Future<Output = Result<(), Self::Error>> + Send {
        let int_prepared = prepared.0;
        let cache = self.cache.clone();
        async move {
            self.inner.store_file(int_prepared, data).await?;
            let mut guard = cache.lock().await;
            guard.insert(prepared.1.local_unix_path.clone(), prepared.1);
            Ok(())
        }
    }

    fn delete_file(&self, info: FileInfo) -> impl Future<Output = Result<(), Self::Error>> + Send {
        let cache = self.cache.clone();
        let path = info.local_unix_path.clone();
        async move {
            self.inner.delete_file(info).await?;
            let mut guard = cache.lock().await;
            guard.remove(&path);
            Ok(())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_try_cache_content() {
        let base = PathBuf::from(".");
        let ignore_parts = vec![];
        let cache_data: [FileInfo; 1] = [FileInfo {
            crc32: 123,
            local_unix_path: "abc".to_string(),
        }];

        let tokio_cached_local_storage = TokioCachedLocalStorage::new(base, ignore_parts, cache_data.to_vec());

        let content = tokio_cached_local_storage.try_cache_content();
        assert_eq!(
            content,
            Some(
                cache_data
                    .iter()
                    .map(|e| (e.local_unix_path.clone(), e.clone()))
                    .collect()
            )
        );
    }
}