use std::fs; use std::path::{Component, Path, PathBuf}; use anyhow::{Result, bail}; use fancy_regex::Regex; use indexmap::IndexSet; use path_absolutize::Absolutize; type ParseGlobResult = (String, Option>, bool, Option); pub fn safe_join_path, T2: AsRef>( base_path: T1, sub_path: T2, ) -> Option { let base_path = base_path.as_ref(); let sub_path = sub_path.as_ref(); if sub_path.is_absolute() { return None; } let mut joined_path = PathBuf::from(base_path); for component in sub_path.components() { if Component::ParentDir == component { return None; } joined_path.push(component); } if joined_path.starts_with(base_path) { Some(joined_path) } else { None } } pub async fn expand_glob_paths>( paths: &[T], bail_non_exist: bool, ) -> Result> { let mut new_paths = IndexSet::new(); for path in paths { let (path_str, suffixes, current_only, depth) = parse_glob(path.as_ref())?; list_files( &mut new_paths, Path::new(&path_str), suffixes.as_ref(), current_only, bail_non_exist, depth, ) .await?; } Ok(new_paths) } pub fn clear_dir(dir: &Path) -> Result<()> { for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() { fs::remove_dir_all(&path)?; } else { fs::remove_file(&path)?; } } Ok(()) } pub fn list_file_names>(dir: T, ext: &str) -> Vec { match fs::read_dir(dir.as_ref()) { Ok(rd) => { let mut names = vec![]; for entry in rd.flatten() { let name = entry.file_name(); if let Some(name) = name.to_string_lossy().strip_suffix(ext) { names.push(name.to_string()); } } names.sort_unstable(); names } Err(_) => vec![], } } pub fn get_patch_extension(path: &str) -> Option { Path::new(&path) .extension() .map(|v| v.to_string_lossy().to_lowercase()) } pub fn to_absolute_path(path: &str) -> Result { Ok(Path::new(&path).absolutize()?.display().to_string()) } pub fn resolve_home_dir(path: &str) -> String { let mut path = path.to_string(); if (path.starts_with("~/") || path.starts_with("~\\")) && let Some(home_dir) = dirs::home_dir() { path.replace_range(..1, &home_dir.display().to_string()); } path } fn parse_glob(path_str: &str) -> Result { let globbed_single_subdir_regex = Regex::new(r"\*/[^/]+\.[^/]+$").expect("invalid regex"); let globbed_recursive_subdir_regex = Regex::new(r"\*\*/[^/]+\.[^/]+$").expect("invalid regex"); let glob_result = if let Some(start) = path_str.find("/**/*.").or_else(|| path_str.find(r"\**\*.")) { Some((start, 6, false, None)) } else if let Some(start) = path_str.find("**/*.").or_else(|| path_str.find(r"**\*.")) { if start == 0 { Some((start, 5, false, None)) } else { None } } else if let Some(m) = globbed_recursive_subdir_regex.find(path_str)? { Some((m.start(), 3, false, None)) } else if let Some(m) = globbed_single_subdir_regex.find(path_str)? { Some((m.start(), 2, false, Some(1usize))) } else if let Some(start) = path_str.find("/*.").or_else(|| path_str.find(r"\*.")) { Some((start, 3, true, None)) } else if let Some(start) = path_str.find("*.") { if start == 0 { Some((start, 2, true, None)) } else { None } } else { None }; if let Some((start, offset, current_only, depth)) = glob_result { let mut base_path = path_str[..start].to_string(); if base_path.is_empty() { base_path = if path_str .chars() .next() .map(|v| v == '/') .unwrap_or_default() { "/" } else { "." } .into(); } let extensions = if let Some(curly_brace_end) = path_str[start..].find('}') { let end = start + curly_brace_end; let extensions_str = &path_str[start + offset..end + 1]; if extensions_str.starts_with('{') && extensions_str.ends_with('}') { extensions_str[1..extensions_str.len() - 1] .split(',') .map(|s| s.to_string()) .collect::>() } else { bail!("Invalid path '{path_str}'"); } } else { let extensions_str = &path_str[start + offset..]; vec![extensions_str.to_string()] }; let extensions = if extensions.is_empty() { None } else { Some(extensions) }; Ok((base_path, extensions, current_only, depth)) } else if path_str.ends_with("/**") || path_str.ends_with(r"\**") { Ok(( path_str[0..path_str.len() - 3].to_string(), None, false, None, )) } else { Ok((path_str.to_string(), None, false, None)) } } #[async_recursion::async_recursion] async fn list_files( files: &mut IndexSet, entry_path: &Path, suffixes: Option<&Vec>, current_only: bool, bail_non_exist: bool, depth: Option, ) -> Result<()> { if !entry_path.exists() { if bail_non_exist { bail!("Not found '{}'", entry_path.display()); } else { return Ok(()); } } if entry_path.is_dir() { let mut reader = tokio::fs::read_dir(entry_path).await?; while let Some(entry) = reader.next_entry().await? { let path = entry.path(); if path.is_dir() { if !current_only { if let Some(remaining_depth) = depth { if remaining_depth > 0 { list_files( files, &path, suffixes, current_only, bail_non_exist, Some(remaining_depth - 1), ) .await?; } } else { list_files(files, &path, suffixes, current_only, bail_non_exist, None) .await?; } } } else { add_file(files, suffixes, &path); } } } else { add_file(files, suffixes, entry_path); } Ok(()) } fn add_file(files: &mut IndexSet, suffixes: Option<&Vec>, path: &Path) { if is_valid_extension(suffixes, path) { let path = path.display().to_string(); if !files.contains(&path) { files.insert(path); } } } fn is_valid_extension(suffixes: Option<&Vec>, path: &Path) -> bool { let filename_regex = Regex::new(r"^.+\.*").unwrap(); if let Some(suffixes) = suffixes && !suffixes.is_empty() { if let Ok(Some(_)) = filename_regex.find(&suffixes.join(",")) { let file_name = path .file_name() .and_then(|v| v.to_str()) .expect("invalid filename") .to_string(); return suffixes.contains(&file_name); } else if let Some(extension) = path.extension().map(|v| v.to_string_lossy().to_string()) { return suffixes.contains(&extension); } return false; } true } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_glob() { assert_eq!( parse_glob("dir").unwrap(), ("dir".into(), None, false, None) ); assert_eq!( parse_glob("dir/**").unwrap(), ("dir".into(), None, false, None) ); assert_eq!( parse_glob("dir/file.md").unwrap(), ("dir/file.md".into(), None, false, None) ); assert_eq!( parse_glob("**/*.md").unwrap(), (".".into(), Some(vec!["md".into()]), false, None) ); assert_eq!( parse_glob("/**/*.md").unwrap(), ("/".into(), Some(vec!["md".into()]), false, None) ); assert_eq!( parse_glob("dir/**/*.md").unwrap(), ("dir".into(), Some(vec!["md".into()]), false, None) ); assert_eq!( parse_glob("dir/**/test.md").unwrap(), ("dir/".into(), Some(vec!["test.md".into()]), false, None) ); assert_eq!( parse_glob("dir/*/test.md").unwrap(), ( "dir/".into(), Some(vec!["test.md".into()]), false, Some(1usize) ) ); assert_eq!( parse_glob("dir/**/*.{md,txt}").unwrap(), ( "dir".into(), Some(vec!["md".into(), "txt".into()]), false, None ) ); assert_eq!( parse_glob("C:\\dir\\**\\*.{md,txt}").unwrap(), ( "C:\\dir".into(), Some(vec!["md".into(), "txt".into()]), false, None ) ); assert_eq!( parse_glob("*.md").unwrap(), (".".into(), Some(vec!["md".into()]), true, None) ); assert_eq!( parse_glob("/*.md").unwrap(), ("/".into(), Some(vec!["md".into()]), true, None) ); assert_eq!( parse_glob("dir/*.md").unwrap(), ("dir".into(), Some(vec!["md".into()]), true, None) ); assert_eq!( parse_glob("dir/*.{md,txt}").unwrap(), ( "dir".into(), Some(vec!["md".into(), "txt".into()]), true, None ) ); assert_eq!( parse_glob("C:\\dir\\*.{md,txt}").unwrap(), ( "C:\\dir".into(), Some(vec!["md".into(), "txt".into()]), true, None ) ); } }