diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 50b0f5a6..3c330a3b 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -39,6 +39,7 @@ default = []
cli = ["merge", "clap"]
merge = ["dep:merge"]
clap = ["dep:clap"]
+fuse = ["dep:fuse_mt"]
webdav = ["dep:dav-server", "dep:futures"]
[package.metadata.docs.rs]
@@ -97,6 +98,7 @@ merge = { version = "0.1.0", optional = true }
# vfs support
dav-server = { version = "0.5.8", default-features = false, optional = true }
+fuse_mt = { version = "0.6", optional = true }
futures = { version = "0.3", optional = true }
runtime-format = "0.1.3"
diff --git a/crates/core/src/vfs.rs b/crates/core/src/vfs.rs
index 9f670a20..63fd1d84 100644
--- a/crates/core/src/vfs.rs
+++ b/crates/core/src/vfs.rs
@@ -1,4 +1,6 @@
mod format;
+#[cfg(feature = "fuse")]
+mod fusefs;
#[cfg(feature = "webdav")]
mod webdavfs;
@@ -12,6 +14,8 @@ use bytes::{Bytes, BytesMut};
use runtime_format::FormatArgs;
use strum::EnumString;
+#[cfg(feature = "fuse")]
+pub use crate::vfs::fusefs::FuseFS;
#[cfg(feature = "webdav")]
/// A struct which enables `WebDAV` access to a [`Vfs`] using [`dav-server`]
pub use crate::vfs::webdavfs::WebDavFS;
@@ -390,6 +394,11 @@ impl Vfs {
Ok(result)
}
+ #[cfg(feature = "fuse")]
+ pub fn into_fuse_fs
(self, repo: Repository
) -> FuseFS
{
+ FuseFS::new(repo, self)
+ }
+
#[cfg(feature = "webdav")]
/// Turn the [`Vfs`] into a [`WebDavFS`]
///
diff --git a/crates/core/src/vfs/fusefs.rs b/crates/core/src/vfs/fusefs.rs
new file mode 100644
index 00000000..b5963ae8
--- /dev/null
+++ b/crates/core/src/vfs/fusefs.rs
@@ -0,0 +1,239 @@
+use super::Vfs;
+
+#[cfg(not(windows))]
+use std::os::unix::prelude::OsStrExt;
+use std::{
+ collections::BTreeMap,
+ ffi::{CString, OsStr},
+ path::Path,
+ sync::RwLock,
+ time::{Duration, SystemTime},
+};
+
+use crate::{
+ repofile::{Node, NodeType},
+ IndexedFull, Repository,
+};
+
+use fuse_mt::{
+ CallbackResult, DirectoryEntry, FileAttr, FileType, FilesystemMT, RequestInfo, ResultData,
+ ResultEmpty, ResultEntry, ResultOpen, ResultReaddir, ResultSlice, ResultXattr, Xattr,
+};
+use itertools::Itertools;
+use nix::libc;
+
+use super::OpenFile;
+
+pub struct FuseFS
{
+ repo: Repository
,
+ vfs: Vfs,
+ open_files: RwLock>,
+ now: SystemTime,
+}
+
+impl FuseFS
{
+ pub(crate) fn new(repo: Repository
, vfs: Vfs) -> Self {
+ let open_files = RwLock::new(BTreeMap::new());
+
+ Self {
+ repo,
+ vfs,
+ open_files,
+ now: SystemTime::now(),
+ }
+ }
+
+ fn node_from_path(&self, path: &Path) -> Result {
+ self.vfs
+ .node_from_path(&self.repo, path)
+ .map_err(|_| libc::ENOENT)
+ }
+
+ fn dir_entries_from_path(&self, path: &Path) -> Result, i32> {
+ self.vfs
+ .dir_entries_from_path(&self.repo, path)
+ .map_err(|_| libc::ENOENT)
+ }
+}
+
+fn node_to_filetype(node: &Node) -> FileType {
+ match node.node_type {
+ NodeType::File => FileType::RegularFile,
+ NodeType::Dir => FileType::Directory,
+ NodeType::Symlink { .. } => FileType::Symlink,
+ NodeType::Chardev { .. } => FileType::CharDevice,
+ NodeType::Dev { .. } => FileType::BlockDevice,
+ NodeType::Fifo => FileType::NamedPipe,
+ NodeType::Socket => FileType::Socket,
+ }
+}
+
+fn node_type_to_rdev(tpe: &NodeType) -> u32 {
+ u32::try_from(match tpe {
+ NodeType::Dev { device } | NodeType::Chardev { device } => *device,
+ _ => 0,
+ })
+ .unwrap()
+}
+
+fn node_to_linktarget(node: &Node) -> Option<&OsStr> {
+ if node.is_symlink() {
+ Some(node.node_type.to_link().as_os_str())
+ } else {
+ None
+ }
+}
+
+fn node_to_file_attr(node: &Node, now: SystemTime) -> FileAttr {
+ FileAttr {
+ /// Size in bytes
+ size: node.meta.size,
+ /// Size in blocks
+ blocks: 0,
+ // Time of last access
+ atime: node.meta.atime.map(SystemTime::from).unwrap_or(now),
+ /// Time of last modification
+ mtime: node.meta.mtime.map(SystemTime::from).unwrap_or(now),
+ /// Time of last metadata change
+ ctime: node.meta.ctime.map(SystemTime::from).unwrap_or(now),
+ /// Time of creation (macOS only)
+ crtime: now,
+ /// Kind of file (directory, file, pipe, etc.)
+ kind: node_to_filetype(node),
+ /// Permissions
+ perm: node.meta.mode.unwrap_or(0o755) as u16,
+ /// Number of hard links
+ nlink: node.meta.links.try_into().unwrap_or(1),
+ /// User ID
+ uid: node.meta.uid.unwrap_or(0),
+ /// Group ID
+ gid: node.meta.gid.unwrap_or(0),
+ /// Device ID (if special file)
+ rdev: node_type_to_rdev(&node.node_type),
+ /// Flags (macOS only; see chflags(2))
+ flags: 0,
+ }
+}
+
+impl FilesystemMT for FuseFS
{
+ fn getattr(&self, _req: RequestInfo, path: &Path, _fh: Option) -> ResultEntry {
+ let node = self.node_from_path(path)?;
+ Ok((Duration::from_secs(1), node_to_file_attr(&node, self.now)))
+ }
+
+ #[cfg(not(windows))]
+ fn readlink(&self, _req: RequestInfo, path: &Path) -> ResultData {
+ let target = node_to_linktarget(&self.node_from_path(path)?)
+ .ok_or(libc::ENOSYS)?
+ .as_bytes()
+ .to_vec();
+
+ Ok(target)
+ }
+
+ fn open(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen {
+ let node = self.node_from_path(path)?;
+ let open = self.repo.open_file(&node).map_err(|_| libc::ENOSYS)?;
+ let fh = {
+ let mut open_files = self.open_files.write().unwrap();
+ let fh = open_files
+ .last_key_value()
+ .map(|(fh, _)| *fh + 1)
+ .unwrap_or(0);
+ _ = open_files.insert(fh, open);
+ fh
+ };
+ Ok((fh, 0))
+ }
+
+ fn release(
+ &self,
+ _req: RequestInfo,
+ _path: &Path,
+ fh: u64,
+ _flags: u32,
+ _lock_owner: u64,
+ _flush: bool,
+ ) -> ResultEmpty {
+ _ = self.open_files.write().unwrap().remove(&fh);
+ Ok(())
+ }
+
+ fn read(
+ &self,
+ _req: RequestInfo,
+ _path: &Path,
+ fh: u64,
+ offset: u64,
+ size: u32,
+
+ callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult,
+ ) -> CallbackResult {
+ if let Some(open_file) = self.open_files.read().unwrap().get(&fh) {
+ if let Ok(data) =
+ self.repo
+ .read_file_at(open_file, offset.try_into().unwrap(), size as usize)
+ {
+ return callback(Ok(&data));
+ }
+ }
+ callback(Err(libc::ENOSYS))
+ }
+
+ fn opendir(&self, _req: RequestInfo, _path: &Path, _flags: u32) -> ResultOpen {
+ Ok((0, 0))
+ }
+
+ fn readdir(&self, _req: RequestInfo, path: &Path, _fh: u64) -> ResultReaddir {
+ let nodes = self.dir_entries_from_path(path)?;
+
+ let result = nodes
+ .into_iter()
+ .map(|node| DirectoryEntry {
+ name: node.name(),
+ kind: node_to_filetype(&node),
+ })
+ .collect();
+ Ok(result)
+ }
+
+ fn releasedir(&self, _req: RequestInfo, _path: &Path, _fh: u64, _flags: u32) -> ResultEmpty {
+ Ok(())
+ }
+
+ fn listxattr(&self, _req: RequestInfo, path: &Path, size: u32) -> ResultXattr {
+ let node = self.node_from_path(path)?;
+ let xattrs = node
+ .meta
+ .extended_attributes
+ .into_iter()
+ // convert into null-terminated [u8]
+ .map(|a| CString::new(a.name).unwrap().into_bytes_with_nul())
+ .concat();
+
+ if size == 0 {
+ Ok(Xattr::Size(u32::try_from(xattrs.len()).unwrap()))
+ } else {
+ Ok(Xattr::Data(xattrs))
+ }
+ }
+
+ fn getxattr(&self, _req: RequestInfo, path: &Path, name: &OsStr, size: u32) -> ResultXattr {
+ let node = self.node_from_path(path)?;
+ match node
+ .meta
+ .extended_attributes
+ .into_iter()
+ .find(|a| name == OsStr::new(&a.name))
+ {
+ None => Err(libc::ENOSYS),
+ Some(attr) => {
+ if size == 0 {
+ Ok(Xattr::Size(u32::try_from(attr.value.len()).unwrap()))
+ } else {
+ Ok(Xattr::Data(attr.value))
+ }
+ }
+ }
+ }
+}