cc_sync_session/sync/
mod.rs1use 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 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 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 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 dirs_to_process.push_back(current_dir.path.clone());
79 }
80
81 while let Some(current_dir) = dirs_to_process.pop_front() {
83 log::debug!("Processing directory contents: {}", current_dir.display());
84
85 let entries = match self.filesystem.list_directory(¤t_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 log::debug!("relative path: {}", relative_path.display());
104 let target_path = target_dir.join(&relative_path);
105
106 if entry.is_directory {
107 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 dirs_to_process.push_back(entry.path.clone());
119 } else {
120 match self.should_copy_file(source_path, &target_path) {
122 Ok(true) => {
123 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 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 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 fs.add_file(source_path, vec![1, 2, 3], SystemTime::now());
193 assert!(syncer.should_copy_file(source_path, target_path).unwrap());
194
195 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 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}