356 lines
10 KiB
Rust
356 lines
10 KiB
Rust
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<Vec<String>>, bool, Option<usize>);
|
|
|
|
pub fn safe_join_path<T1: AsRef<Path>, T2: AsRef<Path>>(
|
|
base_path: T1,
|
|
sub_path: T2,
|
|
) -> Option<PathBuf> {
|
|
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<T: AsRef<str>>(
|
|
paths: &[T],
|
|
bail_non_exist: bool,
|
|
) -> Result<IndexSet<String>> {
|
|
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<T: AsRef<Path>>(dir: T, ext: &str) -> Vec<String> {
|
|
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<String> {
|
|
Path::new(&path)
|
|
.extension()
|
|
.map(|v| v.to_string_lossy().to_lowercase())
|
|
}
|
|
|
|
pub fn to_absolute_path(path: &str) -> Result<String> {
|
|
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<ParseGlobResult> {
|
|
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::<Vec<String>>()
|
|
} 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<String>,
|
|
entry_path: &Path,
|
|
suffixes: Option<&Vec<String>>,
|
|
current_only: bool,
|
|
bail_non_exist: bool,
|
|
depth: Option<usize>,
|
|
) -> 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<String>, suffixes: Option<&Vec<String>>, 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<String>>, 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
|
|
)
|
|
);
|
|
}
|
|
}
|