eva_sdk/
fs.rs

1use bmart::tools::Sorting;
2use eva_common::{EResult, Error};
3use glob_match::glob_match;
4use serde::{Deserialize, Serialize};
5use std::fs::Metadata;
6use std::path::{Path, PathBuf};
7use tokio::fs;
8
9const MAX_LINK_DEPTH: usize = 5;
10
11#[derive(Debug, Sorting, Serialize, Deserialize)]
12#[sorting(id = "path")]
13pub struct Entry {
14    pub path: PathBuf,
15    #[serde(skip)]
16    pub meta: Option<Metadata>,
17    pub kind: Kind,
18}
19
20async fn resolve_symlink_metadata(path: PathBuf) -> Option<Metadata> {
21    let mut source = path;
22    for _ in 0..MAX_LINK_DEPTH {
23        let Ok(target) = fs::read_link(&source).await else {
24            return None;
25        };
26        let Ok(meta) = fs::metadata(&target).await else {
27            return None;
28        };
29        if meta.is_symlink() {
30            source = target;
31        } else {
32            return Some(meta);
33        }
34    }
35    None
36}
37
38#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
39#[serde(rename_all = "lowercase")]
40pub enum Kind {
41    File,
42    Dir,
43    #[default]
44    Any,
45    Unknown,
46}
47
48#[async_recursion::async_recursion]
49async fn list_entries_by_mask(
50    path: &Path,
51    masks: &[&str],
52    kind: Kind,
53    result: &mut Vec<Entry>,
54    recursive: bool,
55) -> Result<(), std::io::Error> {
56    let mut entries = fs::read_dir(path).await?;
57    while let Some(entry) = entries.next_entry().await? {
58        let mut meta = entry.metadata().await?;
59        let entry_kind = if meta.is_file() {
60            Kind::File
61        } else if meta.is_dir() {
62            Kind::Dir
63        } else if meta.is_symlink() {
64            if let Some(target_meta) = resolve_symlink_metadata(entry.path()).await {
65                meta = target_meta;
66                if meta.is_file() {
67                    Kind::File
68                } else if meta.is_dir() {
69                    Kind::Dir
70                } else {
71                    Kind::Unknown
72                }
73            } else {
74                Kind::Unknown
75            }
76        } else {
77            Kind::Unknown
78        };
79        if entry_kind == kind || (kind == Kind::Any && entry_kind != Kind::Unknown) {
80            for mask in masks {
81                if glob_match(mask, &entry.file_name().to_string_lossy()) {
82                    result.push(Entry {
83                        path: entry.path(),
84                        meta: Some(meta),
85                        kind: entry_kind,
86                    });
87                    break;
88                }
89            }
90        }
91        if entry_kind == Kind::Dir && recursive {
92            let mut subdir = path.to_owned();
93            subdir.push(entry.file_name());
94            list_entries_by_mask(&subdir, masks, kind, result, true).await?;
95        }
96    }
97    Ok(())
98}
99
100pub async fn list(
101    path: &Path,
102    masks: &[&str],
103    kind: Kind,
104    recursive: bool,
105    absolute: bool,
106) -> EResult<Vec<Entry>> {
107    let mut entries = Vec::new();
108    if let Err(e) = list_entries_by_mask(path, masks, kind, &mut entries, recursive).await {
109        if e.kind() == std::io::ErrorKind::NotFound {
110            return Err(Error::not_found(path.to_string_lossy()));
111        }
112        return Err(e.into());
113    }
114    let s = path.to_string_lossy();
115    let mut result: Vec<Entry> = if absolute {
116        entries
117    } else {
118        entries
119            .into_iter()
120            .filter_map(|r| {
121                let p = r.path.to_string_lossy();
122                if s.len() + 1 < p.len() {
123                    Some(Entry {
124                        path: Path::new(&p[s.len() + 1..]).to_owned(),
125                        meta: r.meta,
126                        kind: r.kind,
127                    })
128                } else {
129                    None
130                }
131            })
132            .collect()
133    };
134    result.sort();
135    Ok(result)
136}
137
138pub async fn safe_list(
139    path: &Path,
140    masks: &[&str],
141    kind: Kind,
142    recursive: bool,
143    absolute: bool,
144) -> EResult<Vec<Entry>> {
145    if path.to_string_lossy().contains("../") {
146        Err(Error::invalid_params("path can not contain ../"))
147    } else {
148        list(path, masks, kind, recursive, absolute).await
149    }
150}