tokio-fs-ext 0.7.5

Extend tokio fs to be compatible with native and wasm
Documentation
use std::{
    io,
    path::{Path, PathBuf},
};

use js_sys::{Array, JsString};
pub use notify_types::event;
use wasm_bindgen::{
    JsCast,
    prelude::{Closure, wasm_bindgen},
};
use wasm_bindgen_futures::JsFuture;
use web_sys::{FileSystemHandle, FileSystemHandleKind};

use super::{
    super::opfs::{opfs_err, virtualize},
    CreateFileMode, OpenDirType,
};

#[wasm_bindgen]
extern "C" {
    // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemObserver
    #[wasm_bindgen(extends = js_sys::Object, js_name = FileSystemObserver, typescript_type = "FileSystemObserver")]
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub type FileSystemObserver;

    #[wasm_bindgen(constructor, js_class = "FileSystemObserver")]
    pub fn new(callback: &js_sys::Function) -> FileSystemObserver;

    #[wasm_bindgen (method, structural, js_class = "FileSystemObserver", js_name = observe)]
    pub fn observe_file(
        this: &FileSystemObserver,
        target: &web_sys::FileSystemFileHandle,
    ) -> js_sys::Promise;

    #[wasm_bindgen (method, structural, js_class = "FileSystemObserver", js_name = observe)]
    pub fn observe_dir(
        this: &FileSystemObserver,
        handle: &web_sys::FileSystemDirectoryHandle,
    ) -> js_sys::Promise;

    #[wasm_bindgen (method, structural, js_class = "FileSystemObserver", js_name = observe)]
    pub fn observe_dir_with_options(
        this: &FileSystemObserver,
        handle: &web_sys::FileSystemDirectoryHandle,
        options: &FileSystemDirObserverOptions,
    ) -> js_sys::Promise;

    #[wasm_bindgen (method, structural, js_class = "FileSystemObserver", js_name = disconnect)]
    pub fn disconnect(this: &FileSystemObserver);

}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen (extends = js_sys::Object , js_name = FileSystemObserverOptions)]
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub type FileSystemDirObserverOptions;
    #[wasm_bindgen(method, getter = "recursive")]
    pub fn get_recursive(this: &FileSystemDirObserverOptions) -> Option<bool>;
    #[wasm_bindgen(method, setter = "recursive")]
    pub fn set_recursive(this: &FileSystemDirObserverOptions, val: bool);
}
impl FileSystemDirObserverOptions {
    pub fn new() -> Self {
        wasm_bindgen::JsCast::unchecked_into(js_sys::Object::new())
    }
}

impl Default for FileSystemDirObserverOptions {
    fn default() -> Self {
        Self::new()
    }
}

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(extends = js_sys::Object , js_name = FileSystemChangeRecord)]
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub type FileSystemChangeRecord;

    #[wasm_bindgen (method, getter, js_class = "FileSystemChangeRecord", js_name = type)]
    pub fn r#type(this: &FileSystemChangeRecord) -> FileSystemChangeRecordType;

    #[wasm_bindgen(method, getter, structural, js_class = "FileSystemChangeRecord", js_name = relativePathComponents)]
    pub fn relative_path_components(this: &FileSystemChangeRecord) -> Array;

    #[wasm_bindgen(method, getter, structural, js_class = "FileSystemChangeRecord", js_name = changedHandle)]
    pub fn changed_handle(this: &FileSystemChangeRecord) -> Option<FileSystemHandle>;

    #[wasm_bindgen(method, getter, structural, js_class = "FileSystemChangeRecord", js_name = root)]
    pub fn root(this: &FileSystemChangeRecord) -> FileSystemHandle;

}

// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemChangeRecord#type
#[wasm_bindgen]
pub enum FileSystemChangeRecordType {
    Appeared = "appeared",
    Disappeared = "disappeared",
    Errored = "errored",
    Modified = "modified",
    Moved = "moved",
    Unknown = "unknown",
}

fn record_to_event(record: &FileSystemChangeRecord, base_path: &Path) -> io::Result<event::Event> {
    let kind = record.changed_handle().map(|h| h.kind());

    let kind = match record.r#type() {
        FileSystemChangeRecordType::Appeared => event::EventKind::Create(match kind {
            Some(FileSystemHandleKind::File) => event::CreateKind::File,
            Some(FileSystemHandleKind::Directory) => event::CreateKind::Folder,
            _ => event::CreateKind::Any,
        }),
        FileSystemChangeRecordType::Disappeared => event::EventKind::Remove(match kind {
            Some(FileSystemHandleKind::File) => event::RemoveKind::File,
            Some(FileSystemHandleKind::Directory) => event::RemoveKind::Folder,
            _ => event::RemoveKind::Any,
        }),
        FileSystemChangeRecordType::Modified => match kind {
            Some(FileSystemHandleKind::File) => {
                event::EventKind::Modify(event::ModifyKind::Data(event::DataChange::Any))
            }
            Some(FileSystemHandleKind::Directory) => {
                event::EventKind::Modify(event::ModifyKind::Metadata(event::MetadataKind::Any))
            }
            _ => event::EventKind::Modify(event::ModifyKind::Any),
        },
        FileSystemChangeRecordType::Moved => {
            event::EventKind::Modify(event::ModifyKind::Name(event::RenameMode::Any))
        }
        FileSystemChangeRecordType::Unknown => event::EventKind::Other,
        FileSystemChangeRecordType::Errored => event::EventKind::Other,
        FileSystemChangeRecordType::__Invalid => event::EventKind::Other,
    };
    let path = base_path.join(
        record
            .relative_path_components()
            .iter()
            .map(|p| String::from(p.unchecked_ref::<JsString>()))
            .collect::<PathBuf>(),
    );
    Ok(event::Event {
        kind,
        paths: vec![path],
        ..Default::default()
    })
}

pub async fn watch_dir(
    path: impl AsRef<Path>,
    recursive: bool,
    cb: impl Fn(event::Event) + Send + Sync + 'static,
) -> io::Result<()> {
    let base_path = virtualize(path.as_ref())?;

    let observer = FileSystemObserver::new(
        Closure::<dyn Fn(Array)>::new(move |records: Array| {
            records.iter().for_each(|record| {
                let record: FileSystemChangeRecord = record.unchecked_into();
                if let Ok(evt) = record_to_event(&record, &base_path) {
                    cb(evt);
                }
            });
        })
        .into_js_value()
        .unchecked_ref(),
    );

    let dir_handle = super::open_dir(path, OpenDirType::NotCreate).await?;

    let options = FileSystemDirObserverOptions::new();
    options.set_recursive(recursive);
    JsFuture::from(observer.observe_dir_with_options(&dir_handle, &options))
        .await
        .map_err(opfs_err)?;

    Ok(())
}

#[allow(dead_code)]
pub async fn watch_file(
    path: impl AsRef<Path>,
    cb: impl Fn(event::Event) + Send + Sync + 'static,
) -> io::Result<()> {
    let base_path = virtualize(path.as_ref())?;

    let observer = FileSystemObserver::new(
        Closure::<dyn Fn(Array)>::new(move |records: Array| {
            records.iter().for_each(|record| {
                let record: FileSystemChangeRecord = record.unchecked_into();
                if let Ok(evt) = record_to_event(&record, &base_path) {
                    cb(evt);
                }
            });
        })
        .into_js_value()
        .unchecked_ref(),
    );

    let file_handle = super::resolve_file_handle(path, CreateFileMode::NotCreate).await?;

    JsFuture::from(observer.observe_file(&file_handle))
        .await
        .map_err(opfs_err)?;
    Ok(())
}