browser_fs/directory.rs
1use std::borrow::Cow;
2use std::ffi::OsString;
3use std::io::{Error, ErrorKind, Result};
4use std::path::{Component, Path, PathBuf};
5
6use futures_lite::StreamExt;
7use js_sys::JsString;
8use wasm_bindgen::{JsCast, JsValue};
9use wasm_bindgen_futures::stream::JsStream;
10use web_sys::{FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemRemoveOptions};
11
12use crate::{Entry, FileType};
13
14/// An entry in a directory.
15///
16/// A stream of entries in a directory is returned by [`read_dir()`].
17#[derive(Debug)]
18pub struct DirEntry {
19 parent: PathBuf,
20 entry: Entry,
21}
22
23impl DirEntry {
24 /// Returns the bare name of this entry without the leading path.
25 ///
26 /// # Examples
27 ///
28 /// ```no_run
29 /// use futures_lite::stream::StreamExt;
30 ///
31 /// # futures_lite::future::block_on(async {
32 /// let mut dir = browser_fs::read_dir(".").await?;
33 ///
34 /// while let Some(entry) = dir.try_next().await? {
35 /// println!("{}", entry.file_name().to_string_lossy());
36 /// }
37 /// # std::io::Result::Ok(()) });
38 /// ```
39 pub fn file_name(&self) -> OsString {
40 self.entry.name().into()
41 }
42
43 /// Returns the full path to this entry.
44 ///
45 /// The full path is created by joining the original path passed to
46 /// [`read_dir()`] with the name of this entry.
47 ///
48 /// # Examples
49 ///
50 /// ```no_run
51 /// use futures_lite::stream::StreamExt;
52 ///
53 /// # futures_lite::future::block_on(async {
54 /// let mut dir = browser_fs::read_dir(".").await?;
55 ///
56 /// while let Some(entry) = dir.try_next().await? {
57 /// println!("{:?}", entry.path());
58 /// }
59 /// # std::io::Result::Ok(()) });
60 /// ```
61 pub fn path(&self) -> PathBuf {
62 self.parent.join(self.entry.name())
63 }
64
65 /// Reads the file type for this entry.
66 ///
67 /// This function will not traverse symbolic links if this entry points at
68 /// one.
69 ///
70 /// # Errors
71 ///
72 /// An error will be returned in the following situations:
73 ///
74 /// * This entry does not point to an existing file or directory anymore.
75 /// * The current process lacks permissions to read this entry's metadata.
76 /// * Some other I/O error occurred.
77 ///
78 /// # Examples
79 ///
80 /// ```no_run
81 /// use futures_lite::stream::StreamExt;
82 ///
83 /// # futures_lite::future::block_on(async {
84 /// let mut dir = browser_fs::read_dir(".").await?;
85 ///
86 /// while let Some(entry) = dir.try_next().await? {
87 /// println!("{:?}", entry.file_type().await?);
88 /// }
89 /// # std::io::Result::Ok(()) });
90 /// ```
91 pub async fn file_type(&self) -> Result<FileType> {
92 // just to keep in sync with async-fs
93 Ok(self.entry.file_type())
94 }
95}
96
97pub(crate) async fn get_directory(path: &Path) -> Result<FileSystemDirectoryHandle> {
98 let mut parts = split_path(path)?;
99 parts.reverse();
100 let mut latest = crate::root_directory().await?;
101 while let Some(next) = parts.pop() {
102 let promise = latest.get_directory_handle(next.as_ref());
103 latest = crate::resolve(promise).await?;
104 }
105 Ok(latest)
106}
107
108/// Creates a new, empty directory at the provided path
109///
110/// # Errors
111///
112/// This function will return an error in the following situations, but is not
113/// limited to just these cases:
114///
115/// * User lacks permissions to create directory at `path`.
116/// * A parent of the given path doesn't exist. (To create a directory and all
117/// its missing parents at the same time, use the [`create_dir_all`]
118/// function.)
119///
120/// # Examples
121///
122/// ```no_run
123/// use browser_fs::create_dir;
124///
125/// # futures_lite::future::block_on(async {
126/// create_dir("/foo").await?;
127/// # std::io::Result::Ok(()) });
128/// ```
129pub async fn create_dir<P: AsRef<Path>>(path: P) -> Result<()> {
130 let mut parts = split_path(path.as_ref())?;
131 parts.reverse();
132 let opts = FileSystemGetDirectoryOptions::new();
133 let mut latest = crate::root_directory().await?;
134 while let Some(next) = parts.pop() {
135 opts.set_create(parts.is_empty());
136 let promise = latest.get_directory_handle_with_options(next.as_ref(), &opts);
137 latest = crate::resolve(promise).await?;
138 }
139 Ok(())
140}
141
142/// Recursively create a directory and all of its parent components if they
143/// are missing.
144///
145/// # Errors
146///
147/// This function will return an error in the following situations, but is not
148/// limited to just these cases:
149///
150/// * If any directory in the path specified by `path` does not already exist
151/// and it could not be created otherwise. The specific error conditions for
152/// when a directory is being created (after it is determined to not exist)
153/// are outlined by [`fs::create_dir`].
154///
155/// Notable exception is made for situations where any of the directories
156/// specified in the `path` could not be created as it was being created
157/// concurrently. Such cases are considered to be successful. That is, calling
158/// `create_dir_all` concurrently from multiple threads or processes is
159/// guaranteed not to fail due to a race condition with itself.
160///
161/// [`fs::create_dir`]: create_dir
162///
163/// # Examples
164///
165/// ```no_run
166/// use browser_fs::create_dir_all;
167///
168/// # futures_lite::future::block_on(async {
169/// create_dir_all("/foo/bar/baz").await?;
170/// # std::io::Result::Ok(()) });
171/// ```
172pub async fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
173 let mut parts = split_path(path.as_ref())?;
174 parts.reverse();
175 let opts = FileSystemGetDirectoryOptions::new();
176 opts.set_create(true);
177 let mut latest = crate::root_directory().await?;
178 while let Some(next) = parts.pop() {
179 let promise = latest.get_directory_handle_with_options(next.as_ref(), &opts);
180 latest = crate::resolve(promise).await?;
181 }
182 Ok(())
183}
184
185/// Splits a path into components and remove potential parent dirs
186fn split_path(path: &Path) -> Result<Vec<Cow<'_, str>>> {
187 let mut res = Vec::new();
188 for item in path.components() {
189 match item {
190 Component::RootDir => {
191 res.clear();
192 }
193 Component::Normal(name) => {
194 res.push(name.to_string_lossy());
195 }
196 Component::ParentDir => {
197 if res.pop().is_none() {
198 return Err(Error::new(
199 ErrorKind::InvalidInput,
200 "unable to reach provided path",
201 ));
202 }
203 }
204 Component::Prefix(_) => {
205 return Err(Error::new(
206 ErrorKind::InvalidInput,
207 "invalid path with prefix",
208 ))
209 }
210 Component::CurDir => {}
211 }
212 }
213 Ok(res)
214}
215
216/// Removes an empty directory.
217///
218/// Note that this function can only delete an empty directory. If you want to
219/// delete a directory and all of its contents, use [`remove_dir_all()`]
220/// instead.
221///
222/// # Errors
223///
224/// An error will be returned in the following situations:
225///
226/// * `path` is not an existing and empty directory.
227/// * The current process lacks permissions to remove the directory.
228/// * Some other I/O error occurred.
229///
230/// # Examples
231///
232/// ```no_run
233/// # futures_lite::future::block_on(async {
234/// browser_fs::remove_dir("./some/directory").await?;
235/// # std::io::Result::Ok(()) });
236/// ```
237pub async fn remove_dir<P: AsRef<Path>>(path: P) -> Result<()> {
238 let Some(parent) = path.as_ref().parent() else {
239 return Err(Error::new(
240 ErrorKind::InvalidInput,
241 "unable to find parent directory",
242 ));
243 };
244 let Some(name) = path.as_ref().file_name() else {
245 return Err(Error::new(
246 ErrorKind::InvalidInput,
247 "unable to find directory name",
248 ));
249 };
250 let parent = get_directory(parent).await?;
251 let promise = parent.remove_entry(name.to_string_lossy().as_ref());
252 crate::resolve_undefined(promise).await
253}
254
255/// Removes a directory and all of its contents.
256///
257/// # Errors
258///
259/// An error will be returned in the following situations:
260///
261/// * `path` is not an existing directory.
262/// * The current process lacks permissions to remove the directory.
263/// * Some other I/O error occurred.
264///
265/// # Examples
266///
267/// ```no_run
268/// # futures_lite::future::block_on(async {
269/// browser_fs::remove_dir_all("./some/directory").await?;
270/// # std::io::Result::Ok(()) });
271/// ```
272pub async fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
273 let Some(parent) = path.as_ref().parent() else {
274 return Err(Error::new(
275 ErrorKind::InvalidInput,
276 "unable to find parent directory",
277 ));
278 };
279 let Some(name) = path.as_ref().file_name() else {
280 return Err(Error::new(
281 ErrorKind::InvalidInput,
282 "unable to find directory name",
283 ));
284 };
285 let parent = get_directory(parent).await?;
286 let opts = FileSystemRemoveOptions::new();
287 opts.set_recursive(true);
288 let promise = parent.remove_entry_with_options(name.to_string_lossy().as_ref(), &opts);
289 crate::resolve_undefined(promise).await
290}
291
292fn read_dir_entry(parent: &Path, value: JsValue) -> Result<DirEntry> {
293 let array: web_sys::js_sys::Array = value.dyn_into().map_err(crate::from_js_error)?;
294 let _name: JsString = array.get(0).dyn_into().map_err(crate::from_js_error)?;
295 let entry = crate::Entry::try_from_js_value(array.get(1))?;
296
297 Ok(DirEntry {
298 parent: parent.to_path_buf(),
299 entry,
300 })
301}
302
303/// Returns a stream of entries in a directory.
304///
305/// The stream yields items of type [`std::io::Result`]`<`[`DirEntry`]`>`. Note
306/// that I/O errors can occur while reading from the stream.
307///
308/// # Errors
309///
310/// An error will be returned in the following situations:
311///
312/// * `path` does not point to an existing directory.
313/// * The current process lacks permissions to read the contents of the
314/// directory.
315/// * Some other I/O error occurred.
316///
317/// # Examples
318///
319/// ```no_run
320/// # futures_lite::future::block_on(async {
321/// use futures_lite::stream::StreamExt;
322///
323/// let mut entries = browser_fs::read_dir(".").await?;
324///
325/// while let Some(entry) = entries.try_next().await? {
326/// println!("{}", entry.file_name().to_string_lossy());
327/// }
328/// # std::io::Result::Ok(()) });
329/// ```
330pub async fn read_dir<P: AsRef<Path>>(
331 path: P,
332) -> Result<impl futures_lite::Stream<Item = Result<DirEntry>>> {
333 let directory = get_directory(path.as_ref()).await?;
334 let stream = JsStream::from(directory.entries());
335
336 Ok(stream.map(move |entry| {
337 let entry = entry.map_err(crate::from_js_error)?;
338 read_dir_entry(path.as_ref(), entry)
339 }))
340}
341
342#[cfg(test)]
343mod tests {
344 #[test]
345 fn split_path() {
346 let path = std::path::PathBuf::from("/foo/bar/baz");
347 assert_eq!(super::split_path(&path).unwrap(), vec!["foo", "bar", "baz"]);
348 let path = std::path::PathBuf::from("/foo/bar/../baz");
349 assert_eq!(super::split_path(&path).unwrap(), vec!["foo", "baz"]);
350 }
351}