1use async_trait::async_trait;
17use url::Url;
18
19use crate::error::Result;
20
21#[derive(Debug, Clone)]
25pub struct FsDirEntry {
26 pub name: String,
27 pub path: Url,
28 pub is_dir: bool,
29}
30
31#[async_trait]
34pub trait Fs: Send + Sync {
35 async fn read_to_string(&self, url: &Url) -> Result<String>;
37
38 async fn read_dir(&self, url: &Url) -> Result<Vec<FsDirEntry>>;
40
41 async fn is_file(&self, url: &Url) -> bool;
43
44 async fn is_dir(&self, url: &Url) -> bool;
46
47 async fn glob(&self, base: &Url, pattern: &str) -> Result<Vec<FsDirEntry>>;
54}
55
56pub async fn exists(fs: &dyn Fs, url: &Url) -> bool {
60 fs.is_file(url).await || fs.is_dir(url).await
61}
62
63pub async fn walk_dir(fs: &dyn Fs, root: &Url) -> Result<Vec<Url>> {
65 let mut result = Vec::new();
66 let mut stack = vec![root.clone()];
67
68 while let Some(dir) = stack.pop() {
69 if !fs.is_dir(&dir).await {
70 continue;
71 }
72 let entries = fs.read_dir(&dir).await?;
73 for entry in entries {
74 if entry.is_dir {
75 stack.push(entry.path.clone());
76 } else {
77 result.push(entry.path);
78 }
79 }
80 }
81
82 Ok(result)
83}
84
85#[cfg(feature = "tokio")]
91#[derive(Debug, Clone, Copy, Default)]
92pub struct OsFs;
93
94#[cfg(feature = "tokio")]
95fn url_to_path(url: &Url) -> crate::error::Result<std::path::PathBuf> {
96 url.to_file_path()
97 .map_err(|_| crate::error::ComponentsJsError::InvalidUrl(url.to_string()))
98}
99
100#[cfg(feature = "tokio")]
101#[async_trait]
102impl Fs for OsFs {
103 async fn glob(&self, base: &Url, pattern: &str) -> Result<Vec<FsDirEntry>> {
104 let base_path = url_to_path(base)?;
105 let full_pattern = base_path.join(pattern).to_string_lossy().into_owned();
106
107 let mut entries = Vec::new();
108 let paths = glob::glob(&full_pattern)
109 .map_err(|e| crate::error::ComponentsJsError::General(e.to_string()))?;
110 for path in paths {
111 let path = path.map_err(|e| e.into_error())?;
112 let is_dir = path.is_dir();
113 let name = path
114 .file_name()
115 .map(|n| n.to_string_lossy().into_owned())
116 .unwrap_or_default();
117 let entry_url = if is_dir {
118 Url::from_directory_path(&path).map_err(|_| {
119 crate::error::ComponentsJsError::InvalidUrl(path.display().to_string())
120 })?
121 } else {
122 Url::from_file_path(&path).map_err(|_| {
123 crate::error::ComponentsJsError::InvalidUrl(path.display().to_string())
124 })?
125 };
126 entries.push(FsDirEntry {
127 name,
128 path: entry_url,
129 is_dir,
130 });
131 }
132
133 Ok(entries)
134 }
135
136 async fn read_to_string(&self, url: &Url) -> Result<String> {
137 let path = url_to_path(url)?;
138 Ok(tokio::fs::read_to_string(path).await?)
139 }
140
141 async fn read_dir(&self, url: &Url) -> Result<Vec<FsDirEntry>> {
142 let path = url_to_path(url)?;
143 let mut entries = Vec::new();
144 let mut rd = tokio::fs::read_dir(&path).await?;
145 while let Some(entry) = rd.next_entry().await? {
146 let metadata = entry.metadata().await?;
147 let is_dir = metadata.is_dir();
148 let entry_path = entry.path();
149 let entry_url = if is_dir {
150 Url::from_directory_path(&entry_path).map_err(|_| {
151 crate::error::ComponentsJsError::InvalidUrl(entry_path.display().to_string())
152 })?
153 } else {
154 Url::from_file_path(&entry_path).map_err(|_| {
155 crate::error::ComponentsJsError::InvalidUrl(entry_path.display().to_string())
156 })?
157 };
158 entries.push(FsDirEntry {
159 name: entry.file_name().to_string_lossy().into_owned(),
160 path: entry_url,
161 is_dir,
162 });
163 }
164 Ok(entries)
165 }
166
167 async fn is_file(&self, url: &Url) -> bool {
168 match url_to_path(url) {
169 Ok(path) => tokio::fs::metadata(path)
170 .await
171 .map(|m| m.is_file())
172 .unwrap_or(false),
173 Err(_) => false,
174 }
175 }
176
177 async fn is_dir(&self, url: &Url) -> bool {
178 match url_to_path(url) {
179 Ok(path) => tokio::fs::metadata(path)
180 .await
181 .map(|m| m.is_dir())
182 .unwrap_or(false),
183 Err(_) => false,
184 }
185 }
186}