ccsync_core/sync/
orchestrator.rs

1//! Sync orchestration - coordinates the sync workflow
2
3use 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
15/// Approval callback for interactive sync operations
16pub type ApprovalCallback = Box<dyn FnMut(&SyncAction) -> Result<bool>>;
17
18/// Main sync engine
19pub struct SyncEngine {
20    config: Config,
21    /// Sync direction (currently unused, will be used for direction-specific rules and reporting)
22    #[allow(dead_code)]
23    direction: SyncDirection,
24    pattern_matcher: Option<PatternMatcher>,
25}
26
27impl SyncEngine {
28    /// Create a new sync engine
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if pattern compilation fails.
33    pub fn new(config: Config, direction: SyncDirection) -> Result<Self> {
34        // Compile pattern matcher once during construction
35        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    /// Execute the sync operation
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if sync fails.
56    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    /// Execute the sync operation with an optional approval callback
61    ///
62    /// The approver callback is called before executing each action.
63    /// It should return Ok(true) to proceed, Ok(false) to skip, or Err to abort.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if sync fails or approver returns an error.
68    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        // Scan source directory
77        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        // Process each scanned file
82        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            // Apply pattern filter
87            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            // Get relative path
96            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            // Compare files
104            let comparison = FileComparator::compare(&file.path, &dest_path, conflict_strategy)?;
105
106            // Determine action
107            let action = SyncActionResolver::resolve(file.path.clone(), dest_path, &comparison);
108
109            // Skip actions don't need approval (they're automatic decisions)
110            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            // Check approval if callback provided (only for Create and Conflict actions)
119            if let Some(ref mut approve) = approver {
120                match approve(&action) {
121                    Ok(true) => {
122                        // Approved - continue to execution
123                    }
124                    Ok(false) => {
125                        // Skipped by user
126                        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                        // User aborted or error in approval
135                        return Err(e);
136                    }
137                }
138            }
139
140            // Execute action
141            if let Err(e) = executor.execute(&action, &mut result) {
142                eprintln!("Error: {e}");
143                result.errors.push(e.to_string());
144            }
145        }
146
147        // Log warnings from scanner
148        for warning in &scan_result.warnings {
149            eprintln!("Warning: {warning}");
150        }
151
152        // Fail fast if any errors occurred
153        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    /// Get conflict strategy from config or use default
165    const fn get_conflict_strategy(&self) -> ConflictStrategy {
166        match self.config.conflict_strategy {
167            Some(strategy) => strategy,
168            None => ConflictStrategy::Fail,
169        }
170    }
171}