gravityfile_core/
config.rs1use std::path::PathBuf;
4
5use derive_builder::Builder;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Builder, Serialize, Deserialize)]
10#[builder(setter(into), build_fn(validate = "Self::validate"))]
11pub struct ScanConfig {
12 pub root: PathBuf,
14
15 #[builder(default = "false")]
17 #[serde(default)]
18 pub follow_symlinks: bool,
19
20 #[builder(default = "false")]
22 #[serde(default)]
23 pub cross_filesystems: bool,
24
25 #[builder(default = "false")]
27 #[serde(default)]
28 pub apparent_size: bool,
29
30 #[builder(default)]
32 #[serde(default)]
33 pub max_depth: Option<u32>,
34
35 #[builder(default)]
37 #[serde(default)]
38 pub ignore_patterns: Vec<String>,
39
40 #[builder(default = "0")]
42 #[serde(default)]
43 pub threads: usize,
44
45 #[builder(default = "true")]
47 #[serde(default = "default_true")]
48 pub include_hidden: bool,
49
50 #[builder(default = "false")]
52 #[serde(default)]
53 pub compute_hashes: bool,
54
55 #[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 pub fn builder() -> ScanConfigBuilder {
85 ScanConfigBuilder::default()
86 }
87
88 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 pub fn should_ignore(&self, name: &str) -> bool {
106 for pattern in &self.ignore_patterns {
109 if name == pattern {
110 return true;
111 }
112 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 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 assert!(!config.should_skip_hidden(".git"));
186
187 config.include_hidden = false;
189 assert!(config.should_skip_hidden(".git"));
190 assert!(!config.should_skip_hidden("src"));
191 }
192}