use bmart::tools::Sorting;
use eva_common::{EResult, Error};
use glob_match::glob_match;
use serde::{Deserialize, Serialize};
use std::fs::Metadata;
use std::path::{Path, PathBuf};
use tokio::fs;
const MAX_LINK_DEPTH: usize = 5;
#[derive(Debug, Sorting, Serialize, Deserialize)]
#[sorting(id = "path")]
pub struct Entry {
pub path: PathBuf,
#[serde(skip)]
pub meta: Option<Metadata>,
pub kind: Kind,
}
async fn resolve_symlink_metadata(path: PathBuf) -> Option<Metadata> {
let mut source = path;
for _ in 0..MAX_LINK_DEPTH {
let Ok(target) = fs::read_link(&source).await else {
return None;
};
let Ok(meta) = fs::metadata(&target).await else {
return None;
};
if meta.is_symlink() {
source = target;
} else {
return Some(meta);
}
}
None
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
File,
Dir,
#[default]
Any,
Unknown,
}
#[async_recursion::async_recursion]
async fn list_entries_by_mask(
path: &Path,
masks: &[&str],
kind: Kind,
result: &mut Vec<Entry>,
recursive: bool,
) -> Result<(), std::io::Error> {
let mut entries = fs::read_dir(path).await?;
while let Some(entry) = entries.next_entry().await? {
let mut meta = entry.metadata().await?;
let entry_kind = if meta.is_file() {
Kind::File
} else if meta.is_dir() {
Kind::Dir
} else if meta.is_symlink() {
if let Some(target_meta) = resolve_symlink_metadata(entry.path()).await {
meta = target_meta;
if meta.is_file() {
Kind::File
} else if meta.is_dir() {
Kind::Dir
} else {
Kind::Unknown
}
} else {
Kind::Unknown
}
} else {
Kind::Unknown
};
if entry_kind == kind || (kind == Kind::Any && entry_kind != Kind::Unknown) {
for mask in masks {
if glob_match(mask, &entry.file_name().to_string_lossy()) {
result.push(Entry {
path: entry.path(),
meta: Some(meta),
kind: entry_kind,
});
break;
}
}
}
if entry_kind == Kind::Dir && recursive {
let mut subdir = path.to_owned();
subdir.push(entry.file_name());
list_entries_by_mask(&subdir, masks, kind, result, true).await?;
}
}
Ok(())
}
pub async fn list(
path: &Path,
masks: &[&str],
kind: Kind,
recursive: bool,
absolute: bool,
) -> EResult<Vec<Entry>> {
let mut entries = Vec::new();
if let Err(e) = list_entries_by_mask(path, masks, kind, &mut entries, recursive).await {
if e.kind() == std::io::ErrorKind::NotFound {
return Err(Error::not_found(path.to_string_lossy()));
}
return Err(e.into());
}
let s = path.to_string_lossy();
let mut result: Vec<Entry> = if absolute {
entries
} else {
entries
.into_iter()
.filter_map(|r| {
let p = r.path.to_string_lossy();
if s.len() + 1 < p.len() {
Some(Entry {
path: Path::new(&p[s.len() + 1..]).to_owned(),
meta: r.meta,
kind: r.kind,
})
} else {
None
}
})
.collect()
};
result.sort();
Ok(result)
}
pub async fn safe_list(
path: &Path,
masks: &[&str],
kind: Kind,
recursive: bool,
absolute: bool,
) -> EResult<Vec<Entry>> {
if path.to_string_lossy().contains("../") {
Err(Error::invalid_params("path can not contain ../"))
} else {
list(path, masks, kind, recursive, absolute).await
}
}