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 = if is_dir {
105 if dest_path.exists() {
107 let dir_comparison =
109 DirectoryComparator::compare(&file.path, &dest_path)?;
110
111 if dir_comparison.is_identical() {
112 SyncAction::Skip {
113 path: file.path.clone(),
114 reason: "identical content".to_string(),
115 }
116 } else {
117 let source_newer =
119 DirectoryComparator::is_source_newer(&file.path, &dest_path)?;
120 SyncAction::DirectoryConflict {
121 source: file.path.clone(),
122 dest: dest_path,
123 strategy: conflict_strategy,
124 source_newer,
125 }
126 }
127 } else {
128 SyncAction::CreateDirectory {
130 source: file.path.clone(),
131 dest: dest_path,
132 }
133 }
134 } else {
135 let comparison =
137 FileComparator::compare(&file.path, &dest_path, conflict_strategy)?;
138 SyncActionResolver::resolve(file.path.clone(), dest_path, &comparison)
139 };
140
141 if matches!(action, super::actions::SyncAction::Skip { .. }) {
143 if let Err(e) = executor.execute(&action, &mut result) {
144 eprintln!("Error: {e}");
145 result.errors.push(e.to_string());
146 }
147 continue;
148 }
149
150 if let Some(ref mut approve) = approver {
152 match approve(&action) {
153 Ok(true) => {
154 }
156 Ok(false) => {
157 result.skipped += 1;
159 *result
160 .skip_reasons
161 .entry("user skipped".to_string())
162 .or_insert(0) += 1;
163 continue;
164 }
165 Err(e) => {
166 return Err(e);
168 }
169 }
170 }
171
172 if let Err(e) = executor.execute(&action, &mut result) {
174 eprintln!("Error: {e}");
175 result.errors.push(e.to_string());
176 }
177 }
178
179 for warning in &scan_result.warnings {
181 eprintln!("Warning: {warning}");
182 }
183
184 if !result.errors.is_empty() {
186 anyhow::bail!(
187 "Sync failed with {} error(s):\n - {}",
188 result.errors.len(),
189 result.errors.join("\n - ")
190 );
191 }
192
193 Ok(result)
194 }
195
196 const fn get_conflict_strategy(&self) -> ConflictStrategy {
198 match self.config.conflict_strategy {
199 Some(strategy) => strategy,
200 None => ConflictStrategy::Fail,
201 }
202 }
203}