Skip to main content

cc_sync_session/sync/
mod.rs

1use std::path::Path;
2use std::time::SystemTime;
3use std::collections::VecDeque;
4use log::{info, warn};
5
6use crate::filesystem::{FileSystem, FileSystemError, Result};
7
8#[derive(Debug, Clone)]
9pub struct SyncOptions {
10    pub dry_run: bool,
11    pub verbose: bool,
12}
13
14impl Default for SyncOptions {
15    fn default() -> Self {
16        Self {
17            dry_run: false,
18            verbose: false,
19        }
20    }
21}
22
23#[derive(Debug, Default)]
24pub struct SyncResult {
25    pub files_copied: usize,
26    pub files_skipped: usize,
27    pub directories_created: usize,
28    pub errors: Vec<String>,
29}
30
31pub struct SessionSyncer<FS: FileSystem> {
32    filesystem: FS,
33}
34
35impl<FS: FileSystem> SessionSyncer<FS> {
36    pub fn new(filesystem: FS) -> Self {
37        Self { filesystem }
38    }
39    
40    pub fn sync(
41        &self,
42        source_root_dir: &Path,
43        source_prefix: &str,
44        target_dir: &Path,
45        options: &SyncOptions,
46    ) -> Result<SyncResult> {
47        let mut result = SyncResult::default();
48        
49        // Ensure target directory exists
50        if !self.filesystem.exists(target_dir)? {
51            if !options.dry_run {
52                self.filesystem.create_directory(target_dir)?;
53            }
54            result.directories_created += 1;
55            if options.verbose {
56                info!("Created directory: {}", target_dir.display());
57            }
58        }
59        
60        // Find directories that match the source prefix
61        let mut dirs_to_process = VecDeque::new();
62        
63        for current_dir in self.filesystem.list_directory(source_root_dir)? {
64            log::debug!("Processing directory: {}", current_dir.path.display());
65
66            if !current_dir.path.file_name().unwrap().to_string_lossy().starts_with(source_prefix){
67                // Skip directories that don't match the prefix
68                log::debug!("Skipping directory {}: does not match prefix {}", current_dir.path.display(), source_prefix);
69                continue;
70            }
71
72            if !current_dir.is_directory {
73                log::warn!("Skipping non-directory entry: {}", current_dir.path.display());
74                continue;
75            }
76            
77            // Add this directory to be processed
78            dirs_to_process.push_back(current_dir.path.clone());
79        }
80        
81        // Process all directories recursively
82        while let Some(current_dir) = dirs_to_process.pop_front() {
83            log::debug!("Processing directory contents: {}", current_dir.display());
84            
85            // List directory contents
86            let entries = match self.filesystem.list_directory(&current_dir) {
87                Ok(entries) => entries,
88                Err(e) => {
89                    if options.verbose {
90                        warn!("Failed to list directory {}: {}", current_dir.display(), e);
91                    }
92                    continue;
93                }
94            };
95
96            for entry in entries {
97                log::debug!("Processing entry: {:?}", entry.path);
98                let source_path = &entry.path;
99                let relative_path = source_path.strip_prefix(source_root_dir)
100                    .map_err(|e| FileSystemError::PathError(e.to_string()))?;
101                
102                // Convert directory name format
103                log::debug!("relative path: {}", relative_path.display());
104                let target_path = target_dir.join(&relative_path);
105                
106                if entry.is_directory {
107                    // Handle directory
108                    if !self.filesystem.exists(&target_path)? {
109                        if !options.dry_run {
110                            self.filesystem.create_directory(&target_path)?;
111                        }
112                        result.directories_created += 1;
113                        if options.verbose {
114                            info!("Created directory: {}", target_path.display());
115                        }
116                    }
117                    // Add subdirectory to process queue
118                    dirs_to_process.push_back(entry.path.clone());
119                } else {
120                    // Handle file
121                    match self.should_copy_file(source_path, &target_path) {
122                        Ok(true) => {
123                            // Check if parent directory needs to be created
124                            if let Some(parent) = target_path.parent() {
125                                if !self.filesystem.exists(parent)? {
126                                    result.directories_created += 1;
127                                    if !options.dry_run {
128                                        self.filesystem.create_directory(parent)?;
129                                    }
130                                }
131                            }
132                            
133                            if !options.dry_run {
134                                self.filesystem.copy_file(source_path, &target_path)?;
135                                
136                                // Update timestamp to mark as synced
137                                let now = SystemTime::now();
138                                if let Err(e) = self.filesystem.set_modified_time(&target_path, now) {
139                                    if options.verbose {
140                                        warn!("Failed to update timestamp for {}: {}", target_path.display(), e);
141                                    }
142                                }
143                            }
144                            result.files_copied += 1;
145                            info!("Copied: {} -> {}", source_path.display(), target_path.display());
146                        }
147                        Ok(false) => {
148                            result.files_skipped += 1;
149                            info!("Skipped (up to date): {}", source_path.display());
150                        }
151                        Err(e) => {
152                            result.errors.push(format!("Error checking file {}: {}", source_path.display(), e));
153                        }
154                    }
155                }
156            }
157        }
158        
159        Ok(result)
160    }
161    
162    fn should_copy_file(&self, source: &Path, target: &Path) -> Result<bool> {
163        if !self.filesystem.exists(target)? {
164            return Ok(true);
165        }
166        
167        let source_metadata = self.filesystem.get_metadata(source)?;
168        let target_metadata = self.filesystem.get_metadata(target)?;
169        
170        // Copy if source is newer than target
171        Ok(source_metadata.modified > target_metadata.modified)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::mock::MockFileSystem;
179    use std::time::Duration;
180    
181
182    
183    #[test]
184    fn test_should_copy_file() {
185        let fs = MockFileSystem::new();
186        let syncer = SessionSyncer::new(fs.clone());
187        
188        let source_path = Path::new("/source/file.txt");
189        let target_path = Path::new("/target/file.txt");
190        
191        // Test when target doesn't exist
192        fs.add_file(source_path, vec![1, 2, 3], SystemTime::now());
193        assert!(syncer.should_copy_file(source_path, target_path).unwrap());
194        
195        // Test when source is newer
196        let old_time = SystemTime::now() - Duration::from_secs(3600);
197        fs.add_file(target_path, vec![1, 2, 3], old_time);
198        assert!(syncer.should_copy_file(source_path, target_path).unwrap());
199        
200        // Test when target is newer
201        let new_time = SystemTime::now() + Duration::from_secs(3600);
202        fs.set_modified_time(target_path, new_time).unwrap();
203        assert!(!syncer.should_copy_file(source_path, target_path).unwrap());
204    }
205}