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)) + } + } + } + } +}