use std::borrow::Cow;
use std::ffi::OsString;
use std::io::{Error, ErrorKind, Result};
use std::path::{Component, Path, PathBuf};
use futures_lite::StreamExt;
use js_sys::JsString;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::stream::JsStream;
use web_sys::{FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemRemoveOptions};
use crate::{Entry, FileType};
#[derive(Debug)]
pub struct DirEntry {
parent: PathBuf,
entry: Entry,
}
impl DirEntry {
pub fn file_name(&self) -> OsString {
self.entry.name().into()
}
pub fn path(&self) -> PathBuf {
self.parent.join(self.entry.name())
}
pub async fn file_type(&self) -> Result<FileType> {
Ok(self.entry.file_type())
}
}
pub(crate) async fn get_directory(path: &Path) -> Result<FileSystemDirectoryHandle> {
let mut parts = split_path(path)?;
parts.reverse();
let mut latest = crate::root_directory().await?;
while let Some(next) = parts.pop() {
let promise = latest.get_directory_handle(next.as_ref());
latest = crate::resolve(promise).await?;
}
Ok(latest)
}
pub async fn create_dir<P: AsRef<Path>>(path: P) -> Result<()> {
let mut parts = split_path(path.as_ref())?;
parts.reverse();
let opts = FileSystemGetDirectoryOptions::new();
let mut latest = crate::root_directory().await?;
while let Some(next) = parts.pop() {
opts.set_create(parts.is_empty());
let promise = latest.get_directory_handle_with_options(next.as_ref(), &opts);
latest = crate::resolve(promise).await?;
}
Ok(())
}
pub async fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
let mut parts = split_path(path.as_ref())?;
parts.reverse();
let opts = FileSystemGetDirectoryOptions::new();
opts.set_create(true);
let mut latest = crate::root_directory().await?;
while let Some(next) = parts.pop() {
let promise = latest.get_directory_handle_with_options(next.as_ref(), &opts);
latest = crate::resolve(promise).await?;
}
Ok(())
}
fn split_path(path: &Path) -> Result<Vec<Cow<'_, str>>> {
let mut res = Vec::new();
for item in path.components() {
match item {
Component::RootDir => {
res.clear();
}
Component::Normal(name) => {
res.push(name.to_string_lossy());
}
Component::ParentDir => {
if res.pop().is_none() {
return Err(Error::new(
ErrorKind::InvalidInput,
"unable to reach provided path",
));
}
}
Component::Prefix(_) => {
return Err(Error::new(
ErrorKind::InvalidInput,
"invalid path with prefix",
))
}
Component::CurDir => {}
}
}
Ok(res)
}
pub async fn remove_dir<P: AsRef<Path>>(path: P) -> Result<()> {
let Some(parent) = path.as_ref().parent() else {
return Err(Error::new(
ErrorKind::InvalidInput,
"unable to find parent directory",
));
};
let Some(name) = path.as_ref().file_name() else {
return Err(Error::new(
ErrorKind::InvalidInput,
"unable to find directory name",
));
};
let parent = get_directory(parent).await?;
let promise = parent.remove_entry(name.to_string_lossy().as_ref());
crate::resolve_undefined(promise).await
}
pub async fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
let Some(parent) = path.as_ref().parent() else {
return Err(Error::new(
ErrorKind::InvalidInput,
"unable to find parent directory",
));
};
let Some(name) = path.as_ref().file_name() else {
return Err(Error::new(
ErrorKind::InvalidInput,
"unable to find directory name",
));
};
let parent = get_directory(parent).await?;
let opts = FileSystemRemoveOptions::new();
opts.set_recursive(true);
let promise = parent.remove_entry_with_options(name.to_string_lossy().as_ref(), &opts);
crate::resolve_undefined(promise).await
}
fn read_dir_entry(parent: &Path, value: JsValue) -> Result<DirEntry> {
let array: web_sys::js_sys::Array = value.dyn_into().map_err(crate::from_js_error)?;
let _name: JsString = array.get(0).dyn_into().map_err(crate::from_js_error)?;
let entry = crate::Entry::try_from_js_value(array.get(1))?;
Ok(DirEntry {
parent: parent.to_path_buf(),
entry,
})
}
pub async fn read_dir<P: AsRef<Path>>(
path: P,
) -> Result<impl futures_lite::Stream<Item = Result<DirEntry>>> {
let directory = get_directory(path.as_ref()).await?;
let stream = JsStream::from(directory.entries());
Ok(stream.map(move |entry| {
let entry = entry.map_err(crate::from_js_error)?;
read_dir_entry(path.as_ref(), entry)
}))
}
#[cfg(test)]
mod tests {
#[test]
fn split_path() {
let path = std::path::PathBuf::from("/foo/bar/baz");
assert_eq!(super::split_path(&path).unwrap(), vec!["foo", "bar", "baz"]);
let path = std::path::PathBuf::from("/foo/bar/../baz");
assert_eq!(super::split_path(&path).unwrap(), vec!["foo", "baz"]);
}
}