Skip to main content

components_rs/
fs.rs

1use std::path::{Path, PathBuf};
2
3use async_trait::async_trait;
4
5use crate::error::Result;
6
7/// Entry returned by [`Fs::read_dir`].
8#[derive(Debug, Clone)]
9pub struct FsDirEntry {
10    pub name: String,
11    pub path: PathBuf,
12    pub is_dir: bool,
13}
14
15/// Abstract filesystem trait. Implement this to provide a custom backend
16/// (e.g., in-memory for WASM, or virtual for testing).
17#[async_trait]
18pub trait Fs: Send + Sync {
19    /// Read the entire contents of a file as a UTF-8 string.
20    async fn read_to_string(&self, path: &Path) -> Result<String>;
21
22    /// List the immediate children of a directory.
23    async fn read_dir(&self, path: &Path) -> Result<Vec<FsDirEntry>>;
24
25    /// Check whether `path` is a file.
26    async fn is_file(&self, path: &Path) -> bool;
27
28    /// Check whether `path` is a directory.
29    async fn is_dir(&self, path: &Path) -> bool;
30
31    /// Resolve symlinks and produce the canonical, absolute path.
32    /// In environments without symlinks (e.g., WASM), returning the
33    /// input path unchanged is acceptable.
34    async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
35}
36
37// ── Convenience helpers built on top of the trait ────────────────────────
38
39/// Check whether a path exists (file or directory).
40pub async fn exists(fs: &dyn Fs, path: &Path) -> bool {
41    fs.is_file(path).await || fs.is_dir(path).await
42}
43
44/// Recursively walk a directory, returning all file paths.
45/// Follows directories (like `walkdir` with follow_links).
46pub async fn walk_dir(fs: &dyn Fs, root: &Path) -> Result<Vec<PathBuf>> {
47    let mut result = Vec::new();
48    let mut stack = vec![root.to_path_buf()];
49
50    while let Some(dir) = stack.pop() {
51        if !fs.is_dir(&dir).await {
52            continue;
53        }
54        let entries = fs.read_dir(&dir).await?;
55        for entry in entries {
56            if entry.is_dir {
57                stack.push(entry.path.clone());
58            } else {
59                result.push(entry.path);
60            }
61        }
62    }
63
64    Ok(result)
65}
66
67// ── Default implementation backed by the real OS filesystem ─────────────
68
69/// Standard filesystem implementation using `tokio::fs`.
70///
71/// Available only when the `tokio` feature is enabled.
72#[cfg(feature = "tokio")]
73#[derive(Debug, Clone, Copy, Default)]
74pub struct OsFs;
75
76#[cfg(feature = "tokio")]
77#[async_trait]
78impl Fs for OsFs {
79    async fn read_to_string(&self, path: &Path) -> Result<String> {
80        Ok(tokio::fs::read_to_string(path).await?)
81    }
82
83    async fn read_dir(&self, path: &Path) -> Result<Vec<FsDirEntry>> {
84        let mut entries = Vec::new();
85        let mut rd = tokio::fs::read_dir(path).await?;
86        while let Some(entry) = rd.next_entry().await? {
87            let metadata = entry.metadata().await?;
88            entries.push(FsDirEntry {
89                name: entry.file_name().to_string_lossy().into_owned(),
90                path: entry.path(),
91                is_dir: metadata.is_dir(),
92            });
93        }
94        Ok(entries)
95    }
96
97    async fn is_file(&self, path: &Path) -> bool {
98        tokio::fs::metadata(path)
99            .await
100            .map(|m| m.is_file())
101            .unwrap_or(false)
102    }
103
104    async fn is_dir(&self, path: &Path) -> bool {
105        tokio::fs::metadata(path)
106            .await
107            .map(|m| m.is_dir())
108            .unwrap_or(false)
109    }
110
111    async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
112        Ok(tokio::fs::canonicalize(path).await?)
113    }
114}