gravityfile_core/
config.rs

1//! Scan configuration types.
2
3use std::path::PathBuf;
4
5use derive_builder::Builder;
6use serde::{Deserialize, Serialize};
7
8/// Configuration for scanning operations.
9#[derive(Debug, Clone, Builder, Serialize, Deserialize)]
10#[builder(setter(into), build_fn(validate = "Self::validate"))]
11pub struct ScanConfig {
12    /// Root path to scan.
13    pub root: PathBuf,
14
15    /// Follow symbolic links.
16    #[builder(default = "false")]
17    #[serde(default)]
18    pub follow_symlinks: bool,
19
20    /// Cross filesystem boundaries.
21    #[builder(default = "false")]
22    #[serde(default)]
23    pub cross_filesystems: bool,
24
25    /// Use apparent size vs disk usage.
26    #[builder(default = "false")]
27    #[serde(default)]
28    pub apparent_size: bool,
29
30    /// Maximum depth to traverse (None = unlimited).
31    #[builder(default)]
32    #[serde(default)]
33    pub max_depth: Option<u32>,
34
35    /// Patterns to ignore (gitignore syntax).
36    #[builder(default)]
37    #[serde(default)]
38    pub ignore_patterns: Vec<String>,
39
40    /// Number of threads for scanning (0 = auto-detect).
41    #[builder(default = "0")]
42    #[serde(default)]
43    pub threads: usize,
44
45    /// Include hidden files (starting with .).
46    #[builder(default = "true")]
47    #[serde(default = "default_true")]
48    pub include_hidden: bool,
49
50    /// Compute content hashes during scan.
51    #[builder(default = "false")]
52    #[serde(default)]
53    pub compute_hashes: bool,
54
55    /// Minimum file size to hash (skip tiny files).
56    #[builder(default = "4096")]
57    #[serde(default = "default_min_hash_size")]
58    pub min_hash_size: u64,
59}
60
61fn default_true() -> bool {
62    true
63}
64
65fn default_min_hash_size() -> u64 {
66    4096
67}
68
69impl ScanConfigBuilder {
70    fn validate(&self) -> Result<(), String> {
71        if let Some(ref root) = self.root {
72            if root.as_os_str().is_empty() {
73                return Err("Root path cannot be empty".to_string());
74            }
75        } else {
76            return Err("Root path is required".to_string());
77        }
78        Ok(())
79    }
80}
81
82impl ScanConfig {
83    /// Create a new scan config builder.
84    pub fn builder() -> ScanConfigBuilder {
85        ScanConfigBuilder::default()
86    }
87
88    /// Create a simple config for scanning a path.
89    pub fn new(root: impl Into<PathBuf>) -> Self {
90        Self {
91            root: root.into(),
92            follow_symlinks: false,
93            cross_filesystems: false,
94            apparent_size: false,
95            max_depth: None,
96            ignore_patterns: Vec::new(),
97            threads: 0,
98            include_hidden: true,
99            compute_hashes: false,
100            min_hash_size: 4096,
101        }
102    }
103
104    /// Check if a path should be ignored based on patterns.
105    pub fn should_ignore(&self, name: &str) -> bool {
106        // Simple pattern matching for now
107        // TODO: Use gitignore-style matching
108        for pattern in &self.ignore_patterns {
109            if name == pattern {
110                return true;
111            }
112            // Handle glob patterns
113            if pattern.ends_with('*') {
114                let prefix = &pattern[..pattern.len() - 1];
115                if name.starts_with(prefix) {
116                    return true;
117                }
118            }
119            if pattern.starts_with('*') {
120                let suffix = &pattern[1..];
121                if name.ends_with(suffix) {
122                    return true;
123                }
124            }
125        }
126        false
127    }
128
129    /// Check if hidden files should be skipped.
130    pub fn should_skip_hidden(&self, name: &str) -> bool {
131        !self.include_hidden && name.starts_with('.')
132    }
133}
134
135impl Default for ScanConfig {
136    fn default() -> Self {
137        Self::new(".")
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_config_builder() {
147        let config = ScanConfig::builder()
148            .root("/home/user")
149            .threads(4usize)
150            .follow_symlinks(true)
151            .build()
152            .unwrap();
153
154        assert_eq!(config.root, PathBuf::from("/home/user"));
155        assert_eq!(config.threads, 4);
156        assert!(config.follow_symlinks);
157    }
158
159    #[test]
160    fn test_config_simple() {
161        let config = ScanConfig::new("/home/user");
162        assert_eq!(config.root, PathBuf::from("/home/user"));
163        assert!(!config.follow_symlinks);
164        assert_eq!(config.threads, 0);
165    }
166
167    #[test]
168    fn test_should_ignore() {
169        let config = ScanConfig::builder()
170            .root("/test")
171            .ignore_patterns(vec!["node_modules".to_string(), "*.log".to_string()])
172            .build()
173            .unwrap();
174
175        assert!(config.should_ignore("node_modules"));
176        assert!(config.should_ignore("test.log"));
177        assert!(!config.should_ignore("src"));
178    }
179
180    #[test]
181    fn test_should_skip_hidden() {
182        let mut config = ScanConfig::new("/test");
183
184        // By default, hidden files are included
185        assert!(!config.should_skip_hidden(".git"));
186
187        // When hidden files are excluded
188        config.include_hidden = false;
189        assert!(config.should_skip_hidden(".git"));
190        assert!(!config.should_skip_hidden("src"));
191    }
192}