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}