Files
loki/src/utils/path.rs

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
)
);
}
}