ccsync_core/sync/
orchestrator.rs1use std::path::Path;
4
5use anyhow::Context;
6
7use super::SyncResult;
8use super::actions::{SyncAction, SyncActionResolver};
9use super::executor::FileOperationExecutor;
10use crate::comparison::{ConflictStrategy, DirectoryComparator, FileComparator};
11use crate::config::{Config, PatternMatcher, SyncDirection};
12use crate::error::Result;
13use crate::scanner::{FileFilter, Scanner};
14
15pub type ApprovalCallback = Box<dyn FnMut(&SyncAction) -> Result<bool>>;
17
18pub struct SyncEngine {
20 config: Config,
21 #[allow(dead_code)]
23 direction: SyncDirection,
24 pattern_matcher: Option<PatternMatcher>,
25}
26
27impl SyncEngine {
28 pub fn new(config: Config, direction: SyncDirection) -> Result<Self> {
34 let pattern_matcher = if !config.ignore.is_empty() || !config.include.is_empty() {
36 Some(PatternMatcher::with_patterns(
37 &config.ignore,
38 &config.include,
39 )?)
40 } else {
41 None
42 };
43
44 Ok(Self {
45 config,
46 direction,
47 pattern_matcher,
48 })
49 }
50
51 pub fn sync(&self, source_root: &Path, dest_root: &Path) -> Result<SyncResult> {
57 self.sync_with_approver(source_root, dest_root, None)
58 }
59
60 pub fn sync_with_approver(
69 &self,
70 source_root: &Path,
71 dest_root: &Path,
72 mut approver: Option<ApprovalCallback>,
73 ) -> Result<SyncResult> {
74 let mut result = SyncResult::default();
75
76 let filter = FileFilter::new();
78 let scanner = Scanner::new(filter, self.config.preserve_symlinks == Some(true));
79 let scan_result = scanner.scan(source_root);
80
81 let executor = FileOperationExecutor::new(self.config.dry_run == Some(true));
83 let conflict_strategy = self.get_conflict_strategy();
84
85 for file in &scan_result.files {
86 let rel_path = file
88 .path
89 .strip_prefix(source_root)
90 .with_context(|| format!("Failed to strip prefix from {}", file.path.display()))?;
91
92 let is_dir = file.path.is_dir();
94 if let Some(ref matcher) = self.pattern_matcher
95 && !matcher.should_include(rel_path, is_dir)
96 {
97 result.skipped += 1;
98 continue;
99 }
100
101 let dest_path = dest_root.join(rel_path);
102
103 let action = Self::determine_sync_action(&file.path, &dest_path, is_dir, conflict_strategy)?;
105
106 if matches!(action, super::actions::SyncAction::Skip { .. }) {
108 if let Err(e) = executor.execute(&action, &mut result) {
109 eprintln!("Error: {e}");
110 result.errors.push(e.to_string());
111 }
112 continue;
113 }
114
115 match Self::apply_approval(&action, &mut approver, &mut result) {
117 Ok(Some(action_to_execute)) => {
118 if let Err(e) = executor.execute(&action_to_execute, &mut result) {
120 eprintln!("Error: {e}");
121 result.errors.push(e.to_string());
122 }
123 }
124 Ok(None) => {
125 }
127 Err(e) => {
128 return Err(e);
130 }
131 }
132 }
133
134 for warning in &scan_result.warnings {
136 eprintln!("Warning: {warning}");
137 }
138
139 if !result.errors.is_empty() {
141 anyhow::bail!(
142 "Sync failed with {} error(s):\n - {}",
143 result.errors.len(),
144 result.errors.join("\n - ")
145 );
146 }
147
148 Ok(result)
149 }
150
151 const fn get_conflict_strategy(&self) -> ConflictStrategy {
153 match self.config.conflict_strategy {
154 Some(strategy) => strategy,
155 None => ConflictStrategy::Fail,
156 }
157 }
158
159 fn determine_sync_action(
161 source_path: &Path,
162 dest_path: &Path,
163 is_dir: bool,
164 conflict_strategy: ConflictStrategy,
165 ) -> Result<SyncAction> {
166 if is_dir {
167 if dest_path.exists() {
169 let dir_comparison = DirectoryComparator::compare(source_path, dest_path)?;
171
172 if dir_comparison.is_identical() {
173 Ok(SyncAction::Skip {
174 path: source_path.to_path_buf(),
175 reason: "identical content".to_string(),
176 })
177 } else {
178 let source_newer = DirectoryComparator::is_source_newer(source_path, dest_path)?;
180 Ok(SyncAction::DirectoryConflict {
181 source: source_path.to_path_buf(),
182 dest: dest_path.to_path_buf(),
183 strategy: conflict_strategy,
184 source_newer,
185 })
186 }
187 } else {
188 Ok(SyncAction::CreateDirectory {
190 source: source_path.to_path_buf(),
191 dest: dest_path.to_path_buf(),
192 })
193 }
194 } else {
195 let comparison = FileComparator::compare(source_path, dest_path, conflict_strategy)?;
197 Ok(SyncActionResolver::resolve(
198 source_path.to_path_buf(),
199 dest_path.to_path_buf(),
200 &comparison,
201 ))
202 }
203 }
204
205 fn apply_approval(
208 action: &SyncAction,
209 approver: &mut Option<ApprovalCallback>,
210 result: &mut SyncResult,
211 ) -> Result<Option<SyncAction>> {
212 if let Some(approve) = approver {
213 match approve(action) {
214 Ok(true) => {
215 Ok(Some(match action {
217 SyncAction::Conflict {
218 source,
219 dest,
220 strategy: ConflictStrategy::Fail,
221 source_newer,
222 } => SyncAction::Conflict {
223 source: source.clone(),
224 dest: dest.clone(),
225 strategy: ConflictStrategy::Overwrite,
226 source_newer: *source_newer,
227 },
228 SyncAction::DirectoryConflict {
229 source,
230 dest,
231 strategy: ConflictStrategy::Fail,
232 source_newer,
233 } => SyncAction::DirectoryConflict {
234 source: source.clone(),
235 dest: dest.clone(),
236 strategy: ConflictStrategy::Overwrite,
237 source_newer: *source_newer,
238 },
239 _ => action.clone(),
240 }))
241 }
242 Ok(false) => {
243 result.skipped += 1;
245 *result
246 .skip_reasons
247 .entry("user skipped".to_string())
248 .or_insert(0) += 1;
249 Ok(None)
250 }
251 Err(e) => Err(e),
252 }
253 } else {
254 Ok(Some(action.clone()))
255 }
256 }
257}