use super::types::{FileInfo};
use std::path::{Path};
use std::sync::Arc;
use glob::Pattern;
use tracing::debug;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use parking_lot::Mutex;
static EXCLUDE_MATCHER_CACHE: Lazy<Mutex<HashMap<String, Arc<ExcludeMatcher>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
#[derive(Debug, Clone)]
pub struct ExcludeMatcher {
patterns: Vec<Pattern>
}
impl ExcludeMatcher {
pub fn new(exclude_patterns: &[String]) -> anyhow::Result<Self> {
let mut compiled = Vec::with_capacity(exclude_patterns.len());
for pat in exclude_patterns {
let glob_pat = Pattern::new(pat)
.map_err(|e| anyhow::anyhow!("Invalid glob pattern '{}': {}", pat, e))?;
compiled.push(glob_pat);
}
debug!(patterns = ?exclude_patterns, "Compiled {} exclude patterns", compiled.len());
Ok(Self {
patterns: compiled
})
}
pub fn is_excluded(&self, path: &Path, root: &Path) -> bool {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name == ".DS_Store" || file_name.starts_with("._") {
return true;
}
}
let relative = match path.strip_prefix(root) {
Ok(r) => r,
Err(_) => return false, };
let relative_str = relative.to_string_lossy();
for pattern in &self.patterns {
if pattern.matches_path(relative) || pattern.matches(&relative_str) {
return true;
}
}
false
}
}
pub fn should_exclude(path: &Path, root: &Path, exclude_patterns: &[String]) -> bool {
if exclude_patterns.is_empty() {
return false;
}
let mut key_parts = exclude_patterns.to_vec();
key_parts.sort_unstable();
let cache_key = key_parts.join("|");
let matcher = {
let mut cache = EXCLUDE_MATCHER_CACHE.lock();
cache.entry(cache_key.clone()).or_insert_with(|| {
match ExcludeMatcher::new(&exclude_patterns) {
Ok(m) => Arc::new(m),
Err(e) => {
tracing::error!(error = ?e, patterns = ?exclude_patterns, "Failed to compile exclude patterns");
Arc::new(ExcludeMatcher { patterns: vec![] })
}
}
}).clone()
};
matcher.is_excluded(path, root)
}
pub fn should_sync(
source_info: &FileInfo,
target_info: Option<&FileInfo>,
checksum: bool,
) -> bool {
match target_info {
None => true, Some(target) => {
if checksum {
!source_info.content_eq(target)
} else {
source_info.is_newer_than(target)
}
}
}
}