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}