1use crate::conflict::resolve_name_conflicts;
2use crate::error::{FlattenError, Result};
3use crate::file_ops;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7pub struct FlattenConfig {
9 pub input: PathBuf,
11
12 pub output: PathBuf,
14
15 pub preview: bool,
17
18 pub cut: bool,
20
21 pub exclude_extensions: Vec<String>,
23}
24
25impl FlattenConfig {
26 pub fn validate(&self) -> Result<()> {
27 if !self.input.exists() {
28 return Err(FlattenError::SourceNotFound(
29 self.input.display().to_string(),
30 ));
31 }
32
33 if !self.input.is_dir() {
34 return Err(FlattenError::SourceNotDirectory(
35 self.input.display().to_string(),
36 ));
37 }
38
39 if self.output.starts_with(&self.input) {
40 return Err(FlattenError::TargetInsideSource);
41 }
42
43 if !self.preview && !self.output.exists() {
44 std::fs::create_dir_all(&self.output).map_err(|_| {
45 FlattenError::CreateTargetDirFailed(self.output.display().to_string())
46 })?;
47 }
48
49 Ok(())
50 }
51}
52
53pub fn collect_files(input_dir: &Path, exclude_extensions: &[String]) -> Result<Vec<PathBuf>> {
54 let mut files = Vec::new();
55
56 for entry in WalkDir::new(input_dir).into_iter().filter_map(|e| e.ok()) {
57 if entry.file_type().is_file() {
58 let path = entry.path();
59
60 if let Some(extension) = path.extension() {
61 let ext = extension.to_string_lossy().to_lowercase();
62 if exclude_extensions.iter().any(|e| e.to_lowercase() == ext) {
63 continue;
64 }
65 }
66
67 files.push(path.to_path_buf());
68 }
69 }
70
71 Ok(files)
72}
73
74pub fn execute_flatten<F>(config: &FlattenConfig, mut progress_callback: Option<F>) -> Result<usize>
75where
76 F: FnMut(&str, usize, usize),
77{
78 let files = collect_files(&config.input, &config.exclude_extensions)?;
79
80 if files.is_empty() {
81 return Ok(0);
82 }
83
84 let name_map = resolve_name_conflicts(&files);
85 let total = name_map.len();
86
87 for (index, (source_path, target_name)) in name_map.iter().enumerate() {
88 let target_path = config.output.join(target_name);
89
90 if let Some(ref mut callback) = progress_callback {
91 callback(target_name, index + 1, total);
92 }
93
94 if !config.preview {
95 if config.cut {
96 file_ops::move_file(source_path, &target_path)?;
97 } else {
98 file_ops::copy_file(source_path, &target_path)?;
99 }
100 }
101 }
102
103 Ok(total)
104}
105
106pub fn preview_operations(config: &FlattenConfig) -> Result<Vec<(PathBuf, String)>> {
107 let files = collect_files(&config.input, &config.exclude_extensions)?;
108 Ok(resolve_name_conflicts(&files))
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use std::fs;
115 use tempfile::TempDir;
116
117 fn create_test_structure() -> (TempDir, PathBuf) {
118 let temp_dir = TempDir::new().expect("Failed to create temp dir");
119 let source_dir = temp_dir.path().join("source");
120
121 fs::create_dir_all(&source_dir).unwrap();
122 fs::create_dir_all(source_dir.join("docs/notes")).unwrap();
123 fs::create_dir_all(source_dir.join("images/screenshots")).unwrap();
124 fs::create_dir_all(source_dir.join("duplicate")).unwrap();
125
126 fs::write(source_dir.join("file1.txt"), "content1").unwrap();
127 fs::write(source_dir.join("docs/report.pdf"), "pdf content").unwrap();
128 fs::write(source_dir.join("docs/notes/meeting.txt"), "meeting notes").unwrap();
129 fs::write(source_dir.join("images/photo.jpg"), "photo data").unwrap();
130 fs::write(
131 source_dir.join("images/screenshots/screen1.png"),
132 "screenshot1",
133 )
134 .unwrap();
135 fs::write(
136 source_dir.join("images/screenshots/screen2.png"),
137 "screenshot2",
138 )
139 .unwrap();
140 fs::write(source_dir.join("duplicate/file1.txt"), "duplicate content").unwrap();
141
142 (temp_dir, source_dir)
143 }
144
145 #[test]
146 fn test_collect_files() {
147 let (_temp_dir, source_dir) = create_test_structure();
148 let files = collect_files(&source_dir, &[]).unwrap();
149 assert_eq!(files.len(), 7);
150
151 let file_names: Vec<String> = files
152 .iter()
153 .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
154 .collect();
155
156 assert!(file_names.contains(&"file1.txt".to_string()));
157 assert_eq!(
158 file_names
159 .iter()
160 .filter(|&name| name == "file1.txt")
161 .count(),
162 2
163 );
164 }
165
166 #[test]
167 fn test_collect_files_with_exclusion() {
168 let (_temp_dir, source_dir) = create_test_structure();
169
170 let files = collect_files(&source_dir, &["txt".to_string()]).unwrap();
172 assert_eq!(files.len(), 4); let file_names: Vec<String> = files
175 .iter()
176 .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
177 .collect();
178
179 assert!(!file_names.iter().any(|name| name.ends_with(".txt")));
180 assert!(file_names.contains(&"report.pdf".to_string()));
181 assert!(file_names.contains(&"photo.jpg".to_string()));
182 }
183
184 #[test]
185 fn test_flatten_config_validation() {
186 let temp_dir = TempDir::new().unwrap();
187 let source_dir = temp_dir.path().join("source");
188 let target_dir = temp_dir.path().join("target");
189
190 fs::create_dir_all(&source_dir).unwrap();
191
192 let config = FlattenConfig {
193 input: source_dir.clone(),
194 output: target_dir.clone(),
195 preview: false,
196 cut: false,
197 exclude_extensions: vec![],
198 };
199
200 assert!(config.validate().is_ok());
201 }
202
203 #[test]
204 fn test_flatten_config_target_inside_source() {
205 let temp_dir = TempDir::new().unwrap();
206 let source_dir = temp_dir.path().join("source");
207 let target_dir = source_dir.join("target");
208
209 fs::create_dir_all(&source_dir).unwrap();
210
211 let config = FlattenConfig {
212 input: source_dir,
213 output: target_dir,
214 preview: false,
215 cut: false,
216 exclude_extensions: vec![],
217 };
218
219 assert!(matches!(
220 config.validate(),
221 Err(FlattenError::TargetInsideSource)
222 ));
223 }
224
225 #[test]
226 fn test_execute_flatten_copy() {
227 let (_temp_dir, source_dir) = create_test_structure();
228 let target_dir = _temp_dir.path().join("target");
229
230 let config = FlattenConfig {
231 input: source_dir.clone(),
232 output: target_dir.clone(),
233 preview: false,
234 cut: false,
235 exclude_extensions: vec![],
236 };
237
238 config.validate().unwrap();
239 let count = execute_flatten(&config, None::<fn(&str, usize, usize)>).unwrap();
240
241 assert_eq!(count, 7);
242 assert!(target_dir.join("file1.txt").exists());
243 assert!(target_dir.join("report.pdf").exists());
244
245 assert!(source_dir.join("file1.txt").exists());
247 }
248
249 #[test]
250 fn test_preview_operations() {
251 let (_temp_dir, source_dir) = create_test_structure();
252 let target_dir = _temp_dir.path().join("target");
253
254 let config = FlattenConfig {
255 input: source_dir,
256 output: target_dir,
257 preview: true,
258 cut: false,
259 exclude_extensions: vec![],
260 };
261
262 let operations = preview_operations(&config).unwrap();
263 assert_eq!(operations.len(), 7);
264
265 let target_names: Vec<String> = operations.iter().map(|(_, name)| name.clone()).collect();
266
267 assert!(target_names.contains(&"file1.txt".to_string()));
268 assert!(target_names.contains(&"report.pdf".to_string()));
269 }
270}