fskit 0.2.0

Abstractions for building read-only sans-io abstractions VFS
Documentation
use std::fmt::Debug;

use vfs::FileSystem;
use vfs::VfsError;
use vfs::VfsMetadata;
use vfs::error::VfsErrorKind;

use crate::Metadata;
use crate::VfsEntry;
use crate::VfsTree;

/// Implement this to tell [`ReadOnlyVfs`] how to open a file given its metadata.
pub trait FileOpener<F>: Debug + Send + Sync + 'static {
    fn open(&self, meta: &F) -> vfs::VfsResult<Box<dyn vfs::SeekAndRead + Send>>;
}

/// Implement this with a closure/fn for simple cases.
impl<F, Func> FileOpener<F> for Func
where
    Func:
        Fn(&F) -> vfs::VfsResult<Box<dyn vfs::SeekAndRead + Send>> + Debug + Send + Sync + 'static,
{
    fn open(&self, meta: &F) -> vfs::VfsResult<Box<dyn vfs::SeekAndRead + Send>> {
        (self)(meta)
    }
}

/// A ready-made read-only `FileSystem` implementation.
///
/// Wraps a [`VfsTree`] and a [`FileOpener`]. All write methods return
/// `NotSupported`. Consumers only need to provide the file-opening logic.
#[derive(Debug, Clone)]
pub struct ReadOnlyVfs<F, O> {
    tree: VfsTree<F>,
    opener: O,
}

impl<F, O> ReadOnlyVfs<F, O> {
    pub fn new(tree: VfsTree<F>, opener: O) -> Self {
        Self { tree, opener }
    }

    pub fn tree(&self) -> &VfsTree<F> {
        &self.tree
    }

    pub fn opener(&self) -> &O {
        &self.opener
    }
}

impl<F: Metadata + Debug + Send + Sync + 'static, O: FileOpener<F>> FileSystem
    for ReadOnlyVfs<F, O>
{
    fn read_dir(&self, path: &str) -> vfs::VfsResult<Box<dyn Iterator<Item = String> + Send>> {
        self.tree.vfs_read_dir(path)
    }

    fn open_file(&self, path: &str) -> vfs::VfsResult<Box<dyn vfs::SeekAndRead + Send>> {
        let entry = self.tree.vfs_lookup(path)?;
        let VfsEntry::File(meta) = entry else {
            return Err(VfsErrorKind::Other("not a file".into()).into());
        };
        self.opener.open(meta)
    }

    fn metadata(&self, path: &str) -> vfs::VfsResult<VfsMetadata> {
        self.tree.vfs_metadata(path)
    }

    fn exists(&self, path: &str) -> vfs::VfsResult<bool> {
        self.tree.vfs_exists(path)
    }

    crate::read_only_fs_stubs!();
}

impl<F> VfsTree<F> {
    pub fn vfs_lookup(&self, path: &str) -> vfs::VfsResult<&VfsEntry<F>> {
        self.lookup(path)
            .ok_or_else(|| VfsError::from(VfsErrorKind::FileNotFound))
    }

    pub fn vfs_read_dir(
        &self,
        path: &str,
    ) -> vfs::VfsResult<Box<dyn Iterator<Item = String> + Send>> {
        let entry = self.vfs_lookup(path)?;
        match entry {
            VfsEntry::Directory { children, .. } => Ok(Box::new(children.clone().into_iter())),
            VfsEntry::File(_) => Err(VfsError::from(VfsErrorKind::Other(
                "not a directory".into(),
            ))),
        }
    }

    pub fn vfs_exists(&self, path: &str) -> vfs::VfsResult<bool> {
        Ok(self.exists(path))
    }
}

impl<F: Metadata> VfsTree<F> {
    pub fn vfs_metadata(&self, path: &str) -> vfs::VfsResult<VfsMetadata> {
        let entry = self.vfs_lookup(path)?;
        let meta = match entry {
            VfsEntry::Directory { meta, .. } => VfsMetadata {
                file_type: vfs::VfsFileType::Directory,
                len: meta.as_ref().map_or(0, |m| m.len()),
                created: meta.as_ref().and_then(|m| m.created()),
                modified: meta.as_ref().and_then(|m| m.modified()),
                accessed: meta.as_ref().and_then(|m| m.accessed()),
            },
            VfsEntry::File(f) => VfsMetadata {
                file_type: vfs::VfsFileType::File,
                len: f.len(),
                created: f.created(),
                modified: f.modified(),
                accessed: f.accessed(),
            },
        };
        Ok(meta)
    }
}

/// Async equivalent of [`FileOpener`]. Implement this to tell [`ReadOnlyVfs`]
/// how to open a file for async reading.
#[cfg(feature = "async-vfs")]
pub trait AsyncFileOpener<F>: Debug + Send + Sync + 'static {
    fn open_async(
        &self,
        meta: &F,
    ) -> vfs::VfsResult<Box<dyn vfs::async_vfs::SeekAndRead + Send + Unpin>>;
}

#[cfg(feature = "async-vfs")]
impl<F, Func> AsyncFileOpener<F> for Func
where
    Func: Fn(&F) -> vfs::VfsResult<Box<dyn vfs::async_vfs::SeekAndRead + Send + Unpin>>
        + Debug
        + Send
        + Sync
        + 'static,
{
    fn open_async(
        &self,
        meta: &F,
    ) -> vfs::VfsResult<Box<dyn vfs::async_vfs::SeekAndRead + Send + Unpin>> {
        (self)(meta)
    }
}

#[cfg(feature = "async-vfs")]
const _: () = {
    use async_trait::async_trait;
    use vfs::async_vfs::AsyncFileSystem;

    #[async_trait]
    impl<F, O> AsyncFileSystem for ReadOnlyVfs<F, O>
    where
        F: Metadata + Debug + Send + Sync + 'static,
        O: FileOpener<F> + AsyncFileOpener<F>,
    {
        async fn read_dir(
            &self,
            path: &str,
        ) -> vfs::VfsResult<Box<dyn Unpin + futures::Stream<Item = String> + Send>> {
            self.tree.async_vfs_read_dir(path)
        }

        async fn open_file(
            &self,
            path: &str,
        ) -> vfs::VfsResult<Box<dyn vfs::async_vfs::SeekAndRead + Send + Unpin>> {
            let entry = self.tree.vfs_lookup(path)?;
            let VfsEntry::File(meta) = entry else {
                return Err(vfs::error::VfsErrorKind::Other("not a file".into()).into());
            };
            self.opener.open_async(meta)
        }

        async fn metadata(&self, path: &str) -> vfs::VfsResult<VfsMetadata> {
            self.tree.vfs_metadata(path)
        }

        async fn exists(&self, path: &str) -> vfs::VfsResult<bool> {
            self.tree.vfs_exists(path)
        }

        async fn create_dir(&self, _path: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        async fn create_file(
            &self,
            _path: &str,
        ) -> vfs::VfsResult<Box<dyn futures::io::AsyncWrite + Send + Unpin>> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        async fn append_file(
            &self,
            _path: &str,
        ) -> vfs::VfsResult<Box<dyn futures::io::AsyncWrite + Send + Unpin>> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        async fn remove_file(&self, _path: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        async fn remove_dir(&self, _path: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        async fn copy_file(&self, _src: &str, _dest: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        async fn move_file(&self, _src: &str, _dest: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        async fn move_dir(&self, _src: &str, _dest: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }
    }
};

#[cfg(feature = "async-vfs")]
impl<F> VfsTree<F> {
    pub fn async_vfs_read_dir(
        &self,
        path: &str,
    ) -> vfs::VfsResult<Box<dyn Unpin + futures::Stream<Item = String> + Send>> {
        let entry = self.vfs_lookup(path)?;
        match entry {
            VfsEntry::Directory { children, .. } => {
                Ok(Box::new(futures::stream::iter(children.clone())))
            }
            VfsEntry::File(_) => Err(VfsError::from(VfsErrorKind::Other(
                "not a directory".into(),
            ))),
        }
    }
}

/// Generates stubs for all write/mutate methods of `vfs::FileSystem`
/// that return `VfsErrorKind::NotSupported`.
///
/// # Example
///
/// ```ignore
/// impl FileSystem for MyVfs {
///     fn read_dir(&self, path: &str) -> vfs::VfsResult<Box<dyn Iterator<Item = String> + Send>> {
///         self.tree.vfs_read_dir(path)
///     }
///     fn open_file(&self, path: &str) -> vfs::VfsResult<Box<dyn vfs::SeekAndRead + Send>> {
///         todo!()
///     }
///     fn metadata(&self, path: &str) -> vfs::VfsResult<vfs::VfsMetadata> {
///         self.tree.vfs_metadata(path)
///     }
///     fn exists(&self, path: &str) -> vfs::VfsResult<bool> {
///         self.tree.vfs_exists(path)
///     }
///     fskit::read_only_fs_stubs!();
/// }
/// ```
#[macro_export]
macro_rules! read_only_fs_stubs {
    () => {
        fn create_dir(&self, _path: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn create_file(&self, _path: &str) -> vfs::VfsResult<Box<dyn vfs::SeekAndWrite + Send>> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn append_file(&self, _path: &str) -> vfs::VfsResult<Box<dyn vfs::SeekAndWrite + Send>> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn remove_file(&self, _path: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn remove_dir(&self, _path: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn set_creation_time(
            &self,
            _path: &str,
            _time: std::time::SystemTime,
        ) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn set_modification_time(
            &self,
            _path: &str,
            _time: std::time::SystemTime,
        ) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn set_access_time(&self, _path: &str, _time: std::time::SystemTime) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn copy_file(&self, _src: &str, _dest: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn move_file(&self, _src: &str, _dest: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }

        fn move_dir(&self, _src: &str, _dest: &str) -> vfs::VfsResult<()> {
            Err(vfs::error::VfsErrorKind::NotSupported.into())
        }
    };
}