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;
pub struct LocalSimulation {
root_dir: PathBuf,
provider_name: String,
upload_limiter: Option<TokenBucket>,
download_limiter: Option<TokenBucket>,
}
impl LocalSimulation {
pub fn new(root_dir: PathBuf, provider_name: String) -> Self {
Self {
root_dir,
provider_name,
upload_limiter: None,
download_limiter: None,
}
}
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
}
pub fn resolve(&self, remote_path: &str) -> PathBuf {
let normalized = remote_path.trim_start_matches('/');
self.root_dir.join(normalized)
}
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(())
}
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(())
}
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(())
}
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(())
}
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(())
}
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);
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(_))));
let res = sim.delete("missing.txt").await;
assert!(matches!(res, Err(StorageError::NotFound(_))));
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());
let res = sim.rename("missing.txt", "new.txt").await;
assert!(matches!(res, Err(StorageError::NotFound(_))));
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());
let res = sim.list("missing_dir").await;
assert!(matches!(res, Err(StorageError::NotFound(_))));
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());
}
}