subx_cli/cli/input_handler.rs
1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::error::SubXError;
5
6/// Universal input path processing structure for CLI commands.
7///
8/// `InputPathHandler` provides a unified interface for processing file and directory
9/// inputs across different SubX CLI commands. It supports multiple input sources,
10/// recursive directory scanning, and file extension filtering.
11///
12/// This handler is used by commands like `match`, `convert`, `sync`, and `detect-encoding`
13/// to provide consistent `-i` parameter functionality and directory processing behavior.
14///
15/// # Features
16///
17/// - **Multiple Input Sources**: Supports multiple files and directories via `-i` parameter
18/// - **Recursive Processing**: Optional recursive directory scanning with `--recursive` flag
19/// - **File Filtering**: Filter files by extension for command-specific processing
20/// - **Path Validation**: Validates all input paths exist before processing
21/// - **Cross-Platform**: Handles both absolute and relative paths correctly
22///
23/// # Examples
24///
25/// ## Basic Usage
26///
27/// ```rust
28/// use subx_cli::cli::InputPathHandler;
29/// use std::path::PathBuf;
30/// # use tempfile::TempDir;
31/// # use std::fs;
32///
33/// # let tmp = TempDir::new().unwrap();
34/// # let test_dir = tmp.path();
35/// # let file1 = test_dir.join("test1.srt");
36/// # let file2 = test_dir.join("test2.ass");
37/// # fs::write(&file1, "test content").unwrap();
38/// # fs::write(&file2, "test content").unwrap();
39///
40/// // Create handler from multiple paths
41/// let paths = vec![file1, file2];
42/// let handler = InputPathHandler::from_args(&paths, false)?
43/// .with_extensions(&["srt", "ass"]);
44///
45/// // Collect all matching files
46/// let files = handler.collect_files()?;
47/// assert_eq!(files.len(), 2);
48/// # Ok::<(), subx_cli::error::SubXError>(())
49/// ```
50///
51/// ## Directory Processing
52///
53/// ```rust
54/// use subx_cli::cli::InputPathHandler;
55/// use std::path::PathBuf;
56/// # use tempfile::TempDir;
57/// # use std::fs;
58///
59/// # let tmp = TempDir::new().unwrap();
60/// # let test_dir = tmp.path();
61/// # let nested_dir = test_dir.join("nested");
62/// # fs::create_dir(&nested_dir).unwrap();
63/// # let file1 = test_dir.join("test1.srt");
64/// # let file2 = nested_dir.join("test2.srt");
65/// # fs::write(&file1, "test content").unwrap();
66/// # fs::write(&file2, "test content").unwrap();
67///
68/// // Flat directory scanning (non-recursive)
69/// let handler_flat = InputPathHandler::from_args(&[test_dir.to_path_buf()], false)?
70/// .with_extensions(&["srt"]);
71/// let files_flat = handler_flat.collect_files()?;
72/// assert_eq!(files_flat.len(), 1); // Only finds file1
73///
74/// // Recursive directory scanning
75/// let handler_recursive = InputPathHandler::from_args(&[test_dir.to_path_buf()], true)?
76/// .with_extensions(&["srt"]);
77/// let files_recursive = handler_recursive.collect_files()?;
78/// assert_eq!(files_recursive.len(), 2); // Finds both file1 and file2
79/// # Ok::<(), subx_cli::error::SubXError>(())
80/// ```
81///
82/// ## Command Integration
83///
84/// ```rust,no_run
85/// use subx_cli::cli::{InputPathHandler, MatchArgs};
86/// # use std::path::PathBuf;
87///
88/// // Example of how commands use InputPathHandler
89/// # let args = MatchArgs {
90/// # path: Some(PathBuf::from("test")),
91/// # input_paths: vec![],
92/// # recursive: false,
93/// # dry_run: false,
94/// # confidence: 80,
95/// # backup: false,
96/// # copy: false,
97/// # move_files: false,
98/// # };
99/// let handler = args.get_input_handler()?;
100/// let files = handler.collect_files()?;
101/// // Process files...
102/// # Ok::<(), subx_cli::error::SubXError>(())
103/// ```
104#[derive(Debug, Clone)]
105pub struct InputPathHandler {
106 /// List of input paths (files and directories) to process
107 pub paths: Vec<PathBuf>,
108 /// Whether to recursively scan subdirectories
109 pub recursive: bool,
110 /// File extension filters (lowercase, without dot)
111 pub file_extensions: Vec<String>,
112}
113
114impl InputPathHandler {
115 /// Merge paths from multiple sources to create a unified path list
116 ///
117 /// This method provides a unified interface for CLI commands to merge
118 /// different types of path parameters into a single PathBuf vector.
119 ///
120 /// # Arguments
121 ///
122 /// * `optional_paths` - Optional path list (e.g., `path`, `input`, `video`, `subtitle`, etc.)
123 /// * `multiple_paths` - Multiple path list (e.g., `input_paths`)
124 /// * `string_paths` - String format path list (e.g., `file_paths`)
125 ///
126 /// # Returns
127 ///
128 /// Returns the merged PathBuf vector, or an error if all inputs are empty
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// use subx_cli::cli::InputPathHandler;
134 /// use std::path::PathBuf;
135 ///
136 /// // Merge paths from different sources
137 /// let optional = vec![Some(PathBuf::from("single.srt"))];
138 /// let multiple = vec![PathBuf::from("dir1"), PathBuf::from("dir2")];
139 /// let strings = vec!["file1.srt".to_string(), "file2.ass".to_string()];
140 ///
141 /// let merged = InputPathHandler::merge_paths_from_multiple_sources(
142 /// &optional,
143 /// &multiple,
144 /// &strings
145 /// )?;
146 ///
147 /// // merged now contains all paths
148 /// assert_eq!(merged.len(), 5);
149 /// # Ok::<(), subx_cli::error::SubXError>(())
150 /// ```
151 pub fn merge_paths_from_multiple_sources(
152 optional_paths: &[Option<PathBuf>],
153 multiple_paths: &[PathBuf],
154 string_paths: &[String],
155 ) -> Result<Vec<PathBuf>, SubXError> {
156 let mut all_paths = Vec::new();
157
158 // Add optional paths (filter out None values)
159 for p in optional_paths.iter().flatten() {
160 all_paths.push(p.clone());
161 }
162
163 // Add multiple paths
164 all_paths.extend(multiple_paths.iter().cloned());
165
166 // Add string paths (convert to PathBuf)
167 for path_str in string_paths {
168 all_paths.push(PathBuf::from(path_str));
169 }
170
171 // Check if any paths were specified
172 if all_paths.is_empty() {
173 return Err(SubXError::NoInputSpecified);
174 }
175
176 Ok(all_paths)
177 }
178
179 /// Create InputPathHandler from command line arguments
180 pub fn from_args(input_args: &[PathBuf], recursive: bool) -> Result<Self, SubXError> {
181 let handler = Self {
182 paths: input_args.to_vec(),
183 recursive,
184 file_extensions: Vec::new(),
185 };
186 handler.validate()?;
187 Ok(handler)
188 }
189
190 /// Set supported file extensions (without dot)
191 pub fn with_extensions(mut self, extensions: &[&str]) -> Self {
192 self.file_extensions = extensions.iter().map(|s| s.to_lowercase()).collect();
193 self
194 }
195
196 /// Validate that all paths exist
197 pub fn validate(&self) -> Result<(), SubXError> {
198 for path in &self.paths {
199 if !path.exists() {
200 return Err(SubXError::PathNotFound(path.clone()));
201 }
202 }
203 Ok(())
204 }
205
206 /// Get all specified directory paths
207 ///
208 /// This method returns all specified directory paths for commands
209 /// that need to process directories one by one. If the specified path
210 /// contains files, it will return the directory containing that file.
211 ///
212 /// # Returns
213 ///
214 /// Deduplicated list of directory paths
215 ///
216 /// # Examples
217 ///
218 /// ```rust
219 /// use subx_cli::cli::InputPathHandler;
220 /// use std::path::PathBuf;
221 /// # use tempfile::TempDir;
222 /// # use std::fs;
223 ///
224 /// # let tmp = TempDir::new().unwrap();
225 /// # let test_dir = tmp.path();
226 /// # let file1 = test_dir.join("test1.srt");
227 /// # fs::write(&file1, "test content").unwrap();
228 ///
229 /// let paths = vec![file1.clone(), test_dir.to_path_buf()];
230 /// let handler = InputPathHandler::from_args(&paths, false)?;
231 /// let directories = handler.get_directories();
232 ///
233 /// // Should contain test_dir (after deduplication)
234 /// assert_eq!(directories.len(), 1);
235 /// assert_eq!(directories[0], test_dir);
236 /// # Ok::<(), subx_cli::error::SubXError>(())
237 /// ```
238 pub fn get_directories(&self) -> Vec<PathBuf> {
239 let mut directories = std::collections::HashSet::new();
240
241 for path in &self.paths {
242 if path.is_dir() {
243 directories.insert(path.clone());
244 } else if path.is_file() {
245 if let Some(parent) = path.parent() {
246 directories.insert(parent.to_path_buf());
247 }
248 }
249 }
250
251 directories.into_iter().collect()
252 }
253
254 /// Expand files and directories, collecting all files that match the filter conditions
255 pub fn collect_files(&self) -> Result<Vec<PathBuf>, SubXError> {
256 let mut files = Vec::new();
257 for base in &self.paths {
258 if base.is_file() {
259 if self.matches_extension(base) {
260 files.push(base.clone());
261 }
262 } else if base.is_dir() {
263 if self.recursive {
264 files.extend(self.scan_directory_recursive(base)?);
265 } else {
266 files.extend(self.scan_directory_flat(base)?);
267 }
268 } else {
269 return Err(SubXError::InvalidPath(base.clone()));
270 }
271 }
272 Ok(files)
273 }
274
275 fn matches_extension(&self, path: &Path) -> bool {
276 if self.file_extensions.is_empty() {
277 return true;
278 }
279 path.extension()
280 .and_then(|e| e.to_str())
281 .map(|s| {
282 self.file_extensions
283 .iter()
284 .any(|ext| ext.eq_ignore_ascii_case(s))
285 })
286 .unwrap_or(false)
287 }
288
289 fn scan_directory_flat(&self, dir: &Path) -> Result<Vec<PathBuf>, SubXError> {
290 let mut result = Vec::new();
291 let rd = fs::read_dir(dir).map_err(|e| SubXError::DirectoryReadError {
292 path: dir.to_path_buf(),
293 source: e,
294 })?;
295 for entry in rd {
296 let entry = entry.map_err(|e| SubXError::DirectoryReadError {
297 path: dir.to_path_buf(),
298 source: e,
299 })?;
300 let p = entry.path();
301 if p.is_file() && self.matches_extension(&p) {
302 result.push(p);
303 }
304 }
305 Ok(result)
306 }
307
308 fn scan_directory_recursive(&self, dir: &Path) -> Result<Vec<PathBuf>, SubXError> {
309 let mut result = Vec::new();
310 let rd = fs::read_dir(dir).map_err(|e| SubXError::DirectoryReadError {
311 path: dir.to_path_buf(),
312 source: e,
313 })?;
314 for entry in rd {
315 let entry = entry.map_err(|e| SubXError::DirectoryReadError {
316 path: dir.to_path_buf(),
317 source: e,
318 })?;
319 let p = entry.path();
320 if p.is_file() {
321 if self.matches_extension(&p) {
322 result.push(p.clone());
323 }
324 } else if p.is_dir() {
325 result.extend(self.scan_directory_recursive(&p)?);
326 }
327 }
328 Ok(result)
329 }
330}