tokio-fs-ext 0.6.8

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::{
    FileSystemDirectoryHandle, FileSystemHandle, FileSystemHandleKind, FileSystemSyncAccessHandle,
};

use crate::fs::wasm::current_dir;

use super::{super::opfs::OpfsError, CreateFileMode, OpenDirType, SyncAccessMode, open_file};

#[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: &FileSystemSyncAccessHandle,
    ) -> js_sys::Promise;

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

    #[wasm_bindgen (method, structural, js_class = "FileSystemObserver", js_name = observe)]
    pub fn observe_dir_with_options(
        this: &FileSystemObserver,
        handdle: &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>;

}

// 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",
}

impl TryFrom<&FileSystemChangeRecord> for event::Event {
    type Error = io::Error;
    fn try_from(record: &FileSystemChangeRecord) -> Result<Self, Self::Error> {
        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 = current_dir()?.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 observer = FileSystemObserver::new(
        Closure::<dyn Fn(Array)>::new(move |records: Array| {
            records.iter().for_each(|record| {
                let record: FileSystemChangeRecord = record.unchecked_into();
                match event::Event::try_from(&record) {
                    Ok(evt) => cb(evt),
                    Err(_err) => {
                        #[cfg(feature = "opfs_tracing")]
                        tracing::error!("failed to parse event from record: {_err:?}");
                    }
                }
            });
        })
        .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(|e| OpfsError::from(e).into_io_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 observer = FileSystemObserver::new(
        Closure::<dyn Fn(Array)>::new(move |records: Array| {
            records.iter().for_each(|record| {
                let record: FileSystemChangeRecord = record.unchecked_into();
                match event::Event::try_from(&record) {
                    Ok(evt) => cb(evt),
                    Err(_err) => {
                        #[cfg(feature = "opfs_tracing")]
                        tracing::error!("failed to parse event from record: {_err:?}");
                    }
                }
            });
        })
        .into_js_value()
        .unchecked_ref(),
    );

    let file_handle = open_file(
        path,
        CreateFileMode::NotCreate,
        SyncAccessMode::Readonly,
        false,
    )
    .await?
    .sync_access_handle
    .clone();

    JsFuture::from(observer.observe_file(&file_handle))
        .await
        .map_err(|e| OpfsError::from(e).into_io_err())?;
    Ok(())
}