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, 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 is_dir = file.path.is_dir();
88 if let Some(ref matcher) = self.pattern_matcher
89 && !matcher.should_include(&file.path, is_dir)
90 {
91 result.skipped += 1;
92 continue;
93 }
94
95 let rel_path = file
97 .path
98 .strip_prefix(source_root)
99 .with_context(|| format!("Failed to strip prefix from {}", file.path.display()))?;
100
101 let dest_path = dest_root.join(rel_path);
102
103 let comparison = FileComparator::compare(&file.path, &dest_path, conflict_strategy)?;
105
106 let action = SyncActionResolver::resolve(file.path.clone(), dest_path, &comparison);
108
109 if matches!(action, super::actions::SyncAction::Skip { .. }) {
111 if let Err(e) = executor.execute(&action, &mut result) {
112 eprintln!("Error: {e}");
113 result.errors.push(e.to_string());
114 }
115 continue;
116 }
117
118 if let Some(ref mut approve) = approver {
120 match approve(&action) {
121 Ok(true) => {
122 }
124 Ok(false) => {
125 result.skipped += 1;
127 *result
128 .skip_reasons
129 .entry("user skipped".to_string())
130 .or_insert(0) += 1;
131 continue;
132 }
133 Err(e) => {
134 return Err(e);
136 }
137 }
138 }
139
140 if let Err(e) = executor.execute(&action, &mut result) {
142 eprintln!("Error: {e}");
143 result.errors.push(e.to_string());
144 }
145 }
146
147 for warning in &scan_result.warnings {
149 eprintln!("Warning: {warning}");
150 }
151
152 if !result.errors.is_empty() {
154 anyhow::bail!(
155 "Sync failed with {} error(s):\n - {}",
156 result.errors.len(),
157 result.errors.join("\n - ")
158 );
159 }
160
161 Ok(result)
162 }
163
164 const fn get_conflict_strategy(&self) -> ConflictStrategy {
166 match self.config.conflict_strategy {
167 Some(strategy) => strategy,
168 None => ConflictStrategy::Fail,
169 }
170 }
171}