diff --git a/desktop/crates/cloudreve-sync/src/drive/commands.rs b/desktop/crates/cloudreve-sync/src/drive/commands.rs index 0af5f462..9e89d804 100644 --- a/desktop/crates/cloudreve-sync/src/drive/commands.rs +++ b/desktop/crates/cloudreve-sync/src/drive/commands.rs @@ -4,6 +4,7 @@ use crate::{ placeholder::{LocalFileInfo, OpenOptions, PinState}, }, drive::{ + ignore, mounts::Mount, placeholder::CrPlaceholder, sync::{GroupedFsEvents, SyncMode, local_snapshot_differs}, @@ -613,6 +614,25 @@ impl Mount { } pub async fn process_fs_events(&self, events: GroupedFsEvents) -> Result<()> { + // Hot-reload ignore rules when an ignore file was touched in this + // batch, so the new rules apply to the events that follow. + let ignore_file_touched = events.values().flatten().any(|event| { + event.paths.iter().any(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(ignore::is_ignore_file_name) + }) + }); + if ignore_file_touched { + if let Err(err) = self.reload_ignore_patterns().await { + tracing::warn!( + target: "drive::commands", + error = %err, + "Failed to reload ignore patterns" + ); + } + } + // Process groups in a deterministic order: renames/moves first, then // creates and modifications, removes last. A directory move can emit // per-descendant remove events in the same batch; committing the move diff --git a/desktop/crates/cloudreve-sync/src/drive/ignore.rs b/desktop/crates/cloudreve-sync/src/drive/ignore.rs index 476e584d..aef0bcc2 100644 --- a/desktop/crates/cloudreve-sync/src/drive/ignore.rs +++ b/desktop/crates/cloudreve-sync/src/drive/ignore.rs @@ -8,6 +8,27 @@ use anyhow::{Context, Result}; use globset::{Glob, GlobSet, GlobSetBuilder}; use std::path::{Path, PathBuf}; +/// Filenames in the sync root that hold additional ignore patterns, +/// one gitignore-style pattern per line. +pub const IGNORE_FILE_NAMES: &[&str] = &[".cloudreveignore", ".ignore"]; + +/// Returns true if `filename` is an ignore-pattern filename. +pub fn is_ignore_file_name(filename: &str) -> bool { + IGNORE_FILE_NAMES.contains(&filename) +} + +/// Read ignore patterns from ignore files at the sync root. +/// Missing or unreadable files are skipped silently. +pub fn read_ignore_files(sync_root: &Path) -> Vec { + let mut patterns = Vec::new(); + for name in IGNORE_FILE_NAMES { + if let Ok(content) = std::fs::read_to_string(sync_root.join(name)) { + patterns.extend(content.lines().map(str::to_string)); + } + } + patterns +} + /// A wrapper around `GlobSet` for matching ignore patterns (gitignore-style). /// /// The matcher stores the sync root path and automatically strips it from @@ -269,6 +290,26 @@ mod tests { assert!(!matcher.is_match(outside_sync_root_path())); } + #[test] + fn test_read_ignore_files() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + + // No files -> no patterns + assert!(read_ignore_files(root).is_empty()); + + std::fs::write(root.join(".cloudreveignore"), "*.log\n# comment\nbuild/\n").unwrap(); + std::fs::write(root.join(".ignore"), "secret.txt\n").unwrap(); + let patterns = read_ignore_files(root); + assert_eq!(patterns, vec!["*.log", "# comment", "build/", "secret.txt"]); + + // Patterns feed into a working matcher + let matcher = IgnoreMatcher::new(&patterns, root.to_path_buf()).unwrap(); + assert!(matcher.is_match(root.join("debug.log"))); + assert!(matcher.is_match(root.join("secret.txt"))); + assert!(!matcher.is_match(root.join("readme.md"))); + } + #[test] fn test_relative_path_matching() { let sync_root = test_sync_root(); diff --git a/desktop/crates/cloudreve-sync/src/drive/mounts.rs b/desktop/crates/cloudreve-sync/src/drive/mounts.rs index 50ad6679..d9d60d25 100644 --- a/desktop/crates/cloudreve-sync/src/drive/mounts.rs +++ b/desktop/crates/cloudreve-sync/src/drive/mounts.rs @@ -8,7 +8,7 @@ use crate::drive::callback::CallbackHandler; use crate::drive::commands::ManagerCommand; use crate::drive::commands::MountCommand; use crate::drive::event_blocker::EventBlocker; -use crate::drive::ignore::IgnoreMatcher; +use crate::drive::ignore::{self, IgnoreMatcher}; use crate::drive::sync::{SyncMode, group_fs_events}; #[cfg(windows)] use crate::drive::utils::recycle_bin_url; @@ -226,9 +226,9 @@ impl Mount { ) .await; - // Parse ignore patterns from config + // Parse ignore patterns from config and sync-root ignore files let sync_path = config.sync_path.clone(); - let ignore_matcher = match IgnoreMatcher::new(&config.ignore_patterns, sync_path.clone()) { + let ignore_matcher = match build_ignore_matcher(&config.ignore_patterns, &sync_path) { Ok(matcher) => { if !matcher.is_empty() { tracing::info!( @@ -327,12 +327,23 @@ impl Mount { /// Returns an error if any pattern is invalid pub async fn update_ignore_patterns(&self, patterns: Vec) -> Result<()> { let sync_path = self.config.read().await.sync_path.clone(); - let new_matcher = IgnoreMatcher::new(&patterns, sync_path)?; + let new_matcher = build_ignore_matcher(&patterns, &sync_path)?; self.config.write().await.ignore_patterns = patterns; *self.ignore_matcher.write().await = new_matcher; Ok(()) } + /// Rebuild the ignore matcher from config patterns plus any ignore files + /// present at the sync root (`.cloudreveignore` / `.ignore`). + pub async fn reload_ignore_patterns(&self) -> Result<()> { + let (patterns, sync_path) = { + let config = self.config.read().await; + (config.ignore_patterns.clone(), config.sync_path.clone()) + }; + *self.ignore_matcher.write().await = build_ignore_matcher(&patterns, &sync_path)?; + Ok(()) + } + /// Get a copy of the current status flags pub async fn get_status_flags(&self) -> MountStatusFlags { *self.status_flags.lock().await @@ -906,6 +917,14 @@ fn resolve_task_queue_config(config: &DriveConfig) -> TaskQueueConfig { } } +/// Build an ignore matcher from config patterns merged with any ignore files +/// (`.cloudreveignore` / `.ignore`) present at the sync root. +fn build_ignore_matcher(patterns: &[String], sync_root: &Path) -> Result { + let mut all = patterns.to_vec(); + all.extend(ignore::read_ignore_files(sync_root)); + IgnoreMatcher::new(&all, sync_root.to_path_buf()) +} + #[cfg(test)] mod tests { use super::*;