use std::path::Path;
use std::time::SystemTime;
use std::collections::VecDeque;
use log::{info, warn};
use crate::filesystem::{FileSystem, FileSystemError, Result};
#[derive(Debug, Clone)]
pub struct SyncOptions {
pub dry_run: bool,
pub verbose: bool,
}
impl Default for SyncOptions {
fn default() -> Self {
Self {
dry_run: false,
verbose: false,
}
}
}
#[derive(Debug, Default)]
pub struct SyncResult {
pub files_copied: usize,
pub files_skipped: usize,
pub directories_created: usize,
pub errors: Vec<String>,
}
pub struct SessionSyncer<FS: FileSystem> {
filesystem: FS,
}
impl<FS: FileSystem> SessionSyncer<FS> {
pub fn new(filesystem: FS) -> Self {
Self { filesystem }
}
pub fn sync(
&self,
source_root_dir: &Path,
source_prefix: &str,
target_dir: &Path,
options: &SyncOptions,
) -> Result<SyncResult> {
let mut result = SyncResult::default();
if !self.filesystem.exists(target_dir)? {
if !options.dry_run {
self.filesystem.create_directory(target_dir)?;
}
result.directories_created += 1;
if options.verbose {
info!("Created directory: {}", target_dir.display());
}
}
let mut dirs_to_process = VecDeque::new();
for current_dir in self.filesystem.list_directory(source_root_dir)? {
log::debug!("Processing directory: {}", current_dir.path.display());
if !current_dir.path.file_name().unwrap().to_string_lossy().starts_with(source_prefix){
log::debug!("Skipping directory {}: does not match prefix {}", current_dir.path.display(), source_prefix);
continue;
}
if !current_dir.is_directory {
log::warn!("Skipping non-directory entry: {}", current_dir.path.display());
continue;
}
dirs_to_process.push_back(current_dir.path.clone());
}
while let Some(current_dir) = dirs_to_process.pop_front() {
log::debug!("Processing directory contents: {}", current_dir.display());
let entries = match self.filesystem.list_directory(¤t_dir) {
Ok(entries) => entries,
Err(e) => {
if options.verbose {
warn!("Failed to list directory {}: {}", current_dir.display(), e);
}
continue;
}
};
for entry in entries {
log::debug!("Processing entry: {:?}", entry.path);
let source_path = &entry.path;
let relative_path = source_path.strip_prefix(source_root_dir)
.map_err(|e| FileSystemError::PathError(e.to_string()))?;
log::debug!("relative path: {}", relative_path.display());
let target_path = target_dir.join(&relative_path);
if entry.is_directory {
if !self.filesystem.exists(&target_path)? {
if !options.dry_run {
self.filesystem.create_directory(&target_path)?;
}
result.directories_created += 1;
if options.verbose {
info!("Created directory: {}", target_path.display());
}
}
dirs_to_process.push_back(entry.path.clone());
} else {
match self.should_copy_file(source_path, &target_path) {
Ok(true) => {
if let Some(parent) = target_path.parent() {
if !self.filesystem.exists(parent)? {
result.directories_created += 1;
if !options.dry_run {
self.filesystem.create_directory(parent)?;
}
}
}
if !options.dry_run {
self.filesystem.copy_file(source_path, &target_path)?;
let now = SystemTime::now();
if let Err(e) = self.filesystem.set_modified_time(&target_path, now) {
if options.verbose {
warn!("Failed to update timestamp for {}: {}", target_path.display(), e);
}
}
}
result.files_copied += 1;
info!("Copied: {} -> {}", source_path.display(), target_path.display());
}
Ok(false) => {
result.files_skipped += 1;
info!("Skipped (up to date): {}", source_path.display());
}
Err(e) => {
result.errors.push(format!("Error checking file {}: {}", source_path.display(), e));
}
}
}
}
}
Ok(result)
}
fn should_copy_file(&self, source: &Path, target: &Path) -> Result<bool> {
if !self.filesystem.exists(target)? {
return Ok(true);
}
let source_metadata = self.filesystem.get_metadata(source)?;
let target_metadata = self.filesystem.get_metadata(target)?;
Ok(source_metadata.modified > target_metadata.modified)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::MockFileSystem;
use std::time::Duration;
#[test]
fn test_should_copy_file() {
let fs = MockFileSystem::new();
let syncer = SessionSyncer::new(fs.clone());
let source_path = Path::new("/source/file.txt");
let target_path = Path::new("/target/file.txt");
fs.add_file(source_path, vec![1, 2, 3], SystemTime::now());
assert!(syncer.should_copy_file(source_path, target_path).unwrap());
let old_time = SystemTime::now() - Duration::from_secs(3600);
fs.add_file(target_path, vec![1, 2, 3], old_time);
assert!(syncer.should_copy_file(source_path, target_path).unwrap());
let new_time = SystemTime::now() + Duration::from_secs(3600);
fs.set_modified_time(target_path, new_time).unwrap();
assert!(!syncer.should_copy_file(source_path, target_path).unwrap());
}
}