cloud_sync_lib 0.1.0

Cloud storage provider synchronization library
//! Local folder fallback simulator.
//!
//! Provides an implementation of storage simulation on the local filesystem.

use crate::traits::{StorageBackend, StorageItem, StorageError};
use async_trait::async_trait;
use crate::rate_limit::{TokenBucket, copy_rate_limited};
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::info;

/// Local folder fallback simulator.
///
/// Implements mock filesystem storage behavior for offline development and testing
/// when cloud credentials are not configured.
pub struct LocalSimulation {
    /// The root directory representing the simulated remote storage.
    root_dir: PathBuf,
    /// The name of the provider we are simulating (e.g. "Dropbox").
    provider_name: String,
    /// Optional upload rate limiter
    upload_limiter: Option<TokenBucket>,
    /// Optional download rate limiter
    download_limiter: Option<TokenBucket>,
}

impl LocalSimulation {
    /// Creates a new `LocalSimulation` instance with a given root directory and provider name.
    ///
    /// # Arguments
    /// * `root_dir` - The root path to use for simulation.
    /// * `provider_name` - The provider name to mock.
    ///
    /// # Returns
    /// A new instance of `LocalSimulation`.
    pub fn new(root_dir: PathBuf, provider_name: String) -> Self {
        Self {
            root_dir,
            provider_name,
            upload_limiter: None,
            download_limiter: None,
        }
    }

    /// Sets the upload and download rate limiters.
    pub fn with_limiters(
        mut self,
        upload_limiter: Option<TokenBucket>,
        download_limiter: Option<TokenBucket>,
    ) -> Self {
        self.upload_limiter = upload_limiter;
        self.download_limiter = download_limiter;
        self
    }

    /// Maps a remote path to the local directory simulation structure.
    ///
    /// # Arguments
    /// * `remote_path` - The remote path to resolve.
    ///
    /// # Returns
    /// The resolved absolute/relative `PathBuf` under `root_dir`.
    pub fn resolve(&self, remote_path: &str) -> PathBuf {
        let normalized = remote_path.trim_start_matches('/');
        self.root_dir.join(normalized)
    }

    /// Simulates uploading a file by copying it to the local simulation folder.
    ///
    /// # Arguments
    /// * `local_path` - The path to the file on the local machine.
    /// * `remote_path` - The simulated destination path.
    ///
    /// # Returns
    /// An empty `Result`, or a `StorageError` if copying fails.
    pub async fn upload(&self, local_path: &Path, remote_path: &str) -> Result<(), StorageError> {
        let destination = self.resolve(remote_path);
        info!("[{}] (Simulated) Uploading local file {:?} to remote path '{}'", self.provider_name, local_path, remote_path);
        if let Some(parent) = destination.parent() {
            fs::create_dir_all(parent).await?;
        }
        copy_rate_limited(local_path, &destination, self.upload_limiter.clone()).await?;
        if let Ok(metadata) = std::fs::metadata(local_path) {
            if let Ok(modified) = metadata.modified() {
                let ft = filetime::FileTime::from_system_time(modified);
                let _ = filetime::set_file_mtime(&destination, ft);
            }
            let _ = std::fs::set_permissions(&destination, metadata.permissions());
        }
        Ok(())
    }

    /// Simulates downloading a file by copying it from the local simulation folder.
    ///
    /// # Arguments
    /// * `remote_path` - The simulated source path to download from.
    /// * `local_path` - The destination path on the local machine.
    ///
    /// # Returns
    /// An empty `Result`, or a `StorageError` if the file doesn't exist or copying fails.
    pub async fn download(&self, remote_path: &str, local_path: &Path) -> Result<(), StorageError> {
        let source = self.resolve(remote_path);
        info!("[{}] (Simulated) Downloading remote path '{}' to local file {:?}", self.provider_name, remote_path, local_path);
        if !source.exists() {
            return Err(StorageError::NotFound(remote_path.to_string()));
        }
        if let Some(parent) = local_path.parent() {
            fs::create_dir_all(parent).await?;
        }
        copy_rate_limited(&source, local_path, self.download_limiter.clone()).await?;
        if let Ok(metadata) = std::fs::metadata(&source) {
            let _ = std::fs::set_permissions(local_path, metadata.permissions());
        }
        Ok(())
    }

    /// Simulates deleting a file or directory from the local simulation folder.
    ///
    /// # Arguments
    /// * `remote_path` - The simulated path to delete.
    ///
    /// # Returns
    /// An empty `Result`, or a `StorageError` if the file doesn't exist or deletion fails.
    pub async fn delete(&self, remote_path: &str) -> Result<(), StorageError> {
        let target = self.resolve(remote_path);
        info!("[{}] (Simulated) Deleting remote path '{}'", self.provider_name, remote_path);
        if !target.exists() {
            return Err(StorageError::NotFound(remote_path.to_string()));
        }
        if target.is_dir() {
            fs::remove_dir_all(&target).await?;
        } else {
            fs::remove_file(&target).await?;
        }
        Ok(())
    }

    /// Simulates creating a folder/directory in the local simulation folder.
    pub async fn create_folder(&self, remote_path: &str) -> Result<(), StorageError> {
        let target = self.resolve(remote_path);
        info!("[{}] (Simulated) Creating remote directory '{}'", self.provider_name, remote_path);
        fs::create_dir_all(&target).await?;
        Ok(())
    }

    /// Simulates renaming a file or folder in the local simulation folder.
    pub async fn rename(&self, from: &str, to: &str) -> Result<(), StorageError> {
        let source = self.resolve(from);
        let dest = self.resolve(to);
        info!("[{}] (Simulated) Renaming remote path '{}' to '{}'", self.provider_name, from, to);
        if !source.exists() {
            return Err(StorageError::NotFound(from.to_string()));
        }
        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent).await?;
        }
        fs::rename(source, dest).await?;
        Ok(())
    }

    /// Simulates listing contents of the local simulation folder.
    ///
    /// # Arguments
    /// * `remote_path` - The simulated directory path to list.
    ///
    /// # Returns
    /// A vector of `StorageItem` containing metadata for files/folders in the directory, or a `StorageError`.
    pub async fn list(&self, remote_path: &str) -> Result<Vec<StorageItem>, StorageError> {
        let target = self.resolve(remote_path);
        info!("[{}] (Simulated) Listing contents of remote path '{}'", self.provider_name, remote_path);
        if !target.exists() {
            return Err(StorageError::NotFound(remote_path.to_string()));
        }
        let mut items = Vec::new();
        let mut entries = fs::read_dir(&target).await?;
        while let Some(entry) = entries.next_entry().await? {
            let metadata = entry.metadata().await?;
            let is_dir = metadata.is_dir();
            let checksum = if is_dir {
                None
            } else {
                crate::checksum::compute_sha256(&entry.path()).await.ok()
            };
            items.push(StorageItem {
                path: entry.path().strip_prefix(&self.root_dir).unwrap_or(&entry.path()).to_path_buf(),
                size: metadata.len(),
                modified: metadata.modified().unwrap_or(std::time::SystemTime::now()),
                is_dir,
                checksum,
                permissions: cloud_sync_core::get_permissions(&metadata.permissions()),
            });
        }
        Ok(items)
    }
}

#[async_trait]
impl StorageBackend for LocalSimulation {
    fn name(&self) -> &str {
        &self.provider_name
    }

    async fn upload(&self, local_path: &Path, remote_path: &str) -> Result<(), StorageError> {
        self.upload(local_path, remote_path).await
    }

    async fn download(&self, remote_path: &str, local_path: &Path) -> Result<(), StorageError> {
        self.download(remote_path, local_path).await
    }

    async fn delete(&self, remote_path: &str) -> Result<(), StorageError> {
        self.delete(remote_path).await
    }

    async fn list(&self, remote_path: &str) -> Result<Vec<StorageItem>, StorageError> {
        self.list(remote_path).await
    }

    async fn create_folder(&self, remote_path: &str) -> Result<(), StorageError> {
        self.create_folder(remote_path).await
    }

    async fn rename(&self, from: &str, to: &str) -> Result<(), StorageError> {
        self.rename(from, to).await
    }

    async fn compute_local_checksum(&self, local_path: &Path) -> Result<Option<String>, StorageError> {
        Ok(crate::checksum::compute_sha256(local_path).await.ok())
    }
}

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

    #[tokio::test]
    async fn test_local_sim_errors_and_edge_cases() {
        let temp_dir = tempdir().unwrap();
        let sim = LocalSimulation::new(temp_dir.path().to_path_buf(), "MockLocal".to_string())
            .with_limiters(None, None);

        // 1. Download non-existent file
        let download_dest = temp_dir.path().join("download_dest.txt");
        let res = sim.download("missing.txt", &download_dest).await;
        assert!(matches!(res, Err(StorageError::NotFound(_))));

        // 2. Delete non-existent file
        let res = sim.delete("missing.txt").await;
        assert!(matches!(res, Err(StorageError::NotFound(_))));

        // 3. Delete directory
        let dir_to_delete = temp_dir.path().join("dir_to_del");
        std::fs::create_dir(&dir_to_delete).unwrap();
        std::fs::write(dir_to_delete.join("file.txt"), "data").unwrap();
        sim.delete("dir_to_del").await.unwrap();
        assert!(!dir_to_delete.exists());

        // 4. Rename non-existent file
        let res = sim.rename("missing.txt", "new.txt").await;
        assert!(matches!(res, Err(StorageError::NotFound(_))));

        // 5. Rename with non-existent destination parent dir
        let src_file = temp_dir.path().join("src.txt");
        std::fs::write(&src_file, "data").unwrap();
        sim.upload(&src_file, "src.txt").await.unwrap();
        sim.rename("src.txt", "new_parent/dest.txt").await.unwrap();
        assert!(temp_dir.path().join("new_parent/dest.txt").exists());

        // 6. List non-existent directory
        let res = sim.list("missing_dir").await;
        assert!(matches!(res, Err(StorageError::NotFound(_))));

        // 7. compute_local_checksum
        let dest_file = temp_dir.path().join("new_parent/dest.txt");
        let checksum = sim.compute_local_checksum(&dest_file).await.unwrap();
        assert!(checksum.is_some());
    }
}