use crate::ops::SearchOps; use fuser::{ FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen, Request, }; use musicfs_cache::{ Database, OverlayError, OverlayReader, RemoveError, RenameError, VirtualNode, VirtualTree, ROOT_INODE, }; use musicfs_cas::FileReader; use musicfs_core::{Result, VirtualPath}; use parking_lot::RwLock; use std::collections::HashMap; use std::ffi::OsStr; use std::path::Path; use std::sync::Arc; use std::time::{Duration, SystemTime}; use tokio::runtime::Handle; use tracing::{debug, info, instrument, trace, warn}; const TTL: Duration = Duration::from_secs(1); const BLOCK_SIZE: u32 = 512; const SEARCH_QUERY_INODE_BASE: u64 = 0xFFFF_FFFF_0000_0100; pub struct MusicFs { tree: Arc>, reader: Option>, db: Option>, overlay_reader: Option>, runtime_handle: Handle, search_ops: Option, query_inodes: RwLock>, inode_queries: RwLock>, next_query_inode: RwLock, uid: u32, gid: u32, } impl MusicFs { pub fn new(tree: Arc>, runtime_handle: Handle) -> Self { Self { tree, reader: None, db: None, overlay_reader: None, runtime_handle, search_ops: None, query_inodes: RwLock::new(HashMap::new()), inode_queries: RwLock::new(HashMap::new()), next_query_inode: RwLock::new(SEARCH_QUERY_INODE_BASE), uid: unsafe { libc::getuid() }, gid: unsafe { libc::getgid() }, } } pub fn with_reader( tree: Arc>, reader: Arc, runtime_handle: Handle, ) -> Self { Self { tree, reader: Some(reader), db: None, overlay_reader: None, runtime_handle, search_ops: None, query_inodes: RwLock::new(HashMap::new()), inode_queries: RwLock::new(HashMap::new()), next_query_inode: RwLock::new(SEARCH_QUERY_INODE_BASE), uid: unsafe { libc::getuid() }, gid: unsafe { libc::getgid() }, } } pub fn with_db(mut self, db: Arc) -> Self { self.db = Some(db); self } pub fn with_overlay(mut self, overlay: Arc) -> Self { self.overlay_reader = Some(overlay); self } pub fn with_search(mut self, search_ops: SearchOps) -> Self { self.search_ops = Some(search_ops); self } fn resolve_path(&self, parent_inode: u64, name: &OsStr) -> Option { let tree = self.tree.read(); let parent_path = self.inode_to_path_inner(&tree, parent_inode)?; let name_str = name.to_string_lossy(); let full_path = if parent_path == "/" { format!("/{}", name_str) } else { format!("{}/{}", parent_path, name_str) }; Some(VirtualPath::new(full_path)) } fn inode_to_path_inner(&self, tree: &VirtualTree, inode: u64) -> Option { for (path, &ino) in tree.path_to_inode_iter() { if ino == inode { return Some(path.as_str().to_string()); } } None } fn get_or_create_query_inode(&self, query: &str) -> u64 { let query_inodes = self.query_inodes.read(); if let Some(&inode) = query_inodes.get(query) { return inode; } drop(query_inodes); let mut query_inodes = self.query_inodes.write(); let mut inode_queries = self.inode_queries.write(); let mut next_inode = self.next_query_inode.write(); if let Some(&inode) = query_inodes.get(query) { return inode; } let inode = *next_inode; *next_inode += 1; query_inodes.insert(query.to_string(), inode); inode_queries.insert(inode, query.to_string()); inode } fn get_query_for_inode(&self, inode: u64) -> Option { self.inode_queries.read().get(&inode).cloned() } pub fn mount(self, mountpoint: &Path) -> Result<()> { info!("Mounting MusicFS at {:?}", mountpoint); let options = vec![ fuser::MountOption::FSName("musicfs".to_string()), fuser::MountOption::AutoUnmount, fuser::MountOption::AllowOther, ]; fuser::mount2(self, mountpoint, &options).map_err(musicfs_core::Error::Io)?; Ok(()) } pub fn spawn_mount(self, mountpoint: &Path) -> Result { info!("Mounting MusicFS at {:?}", mountpoint); let options = vec![ fuser::MountOption::FSName("musicfs".to_string()), fuser::MountOption::AutoUnmount, fuser::MountOption::AllowOther, ]; let session = fuser::spawn_mount2(self, mountpoint, &options).map_err(musicfs_core::Error::Io)?; Ok(session) } fn node_to_attr(&self, node: &VirtualNode) -> FileAttr { match node { VirtualNode::Directory(dir) => FileAttr { ino: dir.inode, size: 0, blocks: 0, atime: dir.mtime, mtime: dir.mtime, ctime: dir.mtime, crtime: dir.mtime, kind: FileType::Directory, perm: 0o755, nlink: 2, uid: self.uid, gid: self.gid, rdev: 0, blksize: BLOCK_SIZE, flags: 0, }, VirtualNode::File(file) => FileAttr { ino: file.inode, size: file.size, blocks: (file.size + BLOCK_SIZE as u64 - 1) / BLOCK_SIZE as u64, atime: file.mtime, mtime: file.mtime, ctime: file.mtime, crtime: file.mtime, kind: FileType::RegularFile, perm: 0o644, nlink: 1, uid: self.uid, gid: self.gid, rdev: 0, blksize: BLOCK_SIZE, flags: 0, }, } } } impl Filesystem for MusicFs { fn init( &mut self, _req: &Request<'_>, _config: &mut fuser::KernelConfig, ) -> std::result::Result<(), libc::c_int> { info!("MusicFS initialized"); Ok(()) } fn destroy(&mut self) { info!("MusicFS destroyed"); } #[instrument(level = "debug", skip(self, reply))] fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { let name_str = name.to_string_lossy(); if parent == ROOT_INODE && SearchOps::is_search_dir_name(&name_str) { trace!(parent, name = %name_str, "search_dir_name matched"); if let Some(ref search_ops) = self.search_ops { search_ops.lookup_search_dir(reply); return; } } if parent == SearchOps::search_dir_inode() { trace!(parent, name = %name_str, "search_dir_inode matched"); if let Some(ref search_ops) = self.search_ops { let inode = self.get_or_create_query_inode(&name_str); search_ops.lookup_query_dir(&name_str, inode, reply); return; } } if let Some(query) = self.get_query_for_inode(parent) { trace!(parent, name = %name_str, query = %query, "query_inode matched"); if let Some(ref search_ops) = self.search_ops { let inode = self.get_or_create_query_inode(&format!("{}:{}", query, name_str)); search_ops.lookup_result(inode, reply); return; } } let tree = self.tree.read(); if let Some(inode) = tree.lookup(parent, name) { trace!(parent, name = %name_str, ino = inode, "file found in tree"); if let Some(node) = tree.get(inode) { let attr = self.node_to_attr(node); reply.entry(&TTL, &attr, 0); return; } } trace!(parent, name = %name_str, "file not found"); reply.error(libc::ENOENT); } #[instrument(level = "debug", skip(self, reply))] fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { if ino == SearchOps::search_dir_inode() { trace!(ino, "search_dir_inode matched"); if let Some(ref search_ops) = self.search_ops { search_ops.getattr_search_dir(reply); return; } } if SearchOps::is_search_inode(ino) { trace!(ino, "search_inode matched"); if let Some(ref search_ops) = self.search_ops { search_ops.getattr_result(ino, reply); return; } } if self.get_query_for_inode(ino).is_some() { trace!(ino, "query_inode matched"); if let Some(ref search_ops) = self.search_ops { search_ops.getattr_search_dir(reply); return; } } let tree = self.tree.read(); if let Some(node) = tree.get(ino) { trace!(ino, "inode found in tree"); let mut attr = self.node_to_attr(node); if let VirtualNode::File(file) = node { if let Some(ref overlay) = self.overlay_reader { match overlay.estimate_virtual_size(file.file_id) { Ok(Some(virtual_size)) => { trace!(ino, file_id = ?file.file_id, virtual_size, "using overlay virtual size"); attr.size = virtual_size; attr.blocks = (virtual_size + BLOCK_SIZE as u64 - 1) / BLOCK_SIZE as u64; } Ok(None) => { trace!(ino, file_id = ?file.file_id, "no overlay, using original size"); } Err(e) => { warn!(ino, file_id = ?file.file_id, error = %e, "overlay size estimation failed, using original"); } } } } reply.attr(&TTL, &attr); } else { trace!(ino, "inode not found"); reply.error(libc::ENOENT); } } #[instrument(level = "debug", skip(self, reply))] fn readdir( &mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory, ) { if ino == SearchOps::search_dir_inode() { trace!(ino, offset, "search_dir_inode matched"); if let Some(ref search_ops) = self.search_ops { search_ops.readdir_search_root(offset, reply); return; } } if let Some(query) = self.get_query_for_inode(ino) { trace!(ino, offset, query = %query, "query_inode matched"); if let Some(ref search_ops) = self.search_ops { search_ops.readdir_query(&query, offset, reply); return; } } let tree = self.tree.read(); if let Some(children) = tree.readdir(ino) { trace!( ino, offset, children_count = children.len(), "directory found" ); let parent_ino = tree.get_parent(ino).unwrap_or(ROOT_INODE); let entries: Vec<(u64, FileType, &str)> = vec![ (ino, FileType::Directory, "."), (parent_ino, FileType::Directory, ".."), ]; let child_entries: Vec<(u64, FileType, String)> = children .iter() .map(|(name, child_ino, is_dir)| { let kind = if *is_dir { FileType::Directory } else { FileType::RegularFile }; (*child_ino, kind, name.to_string_lossy().to_string()) }) .collect(); for (i, (inode, kind, name)) in entries.iter().enumerate().skip(offset as usize) { if reply.add(*inode, (i + 1) as i64, *kind, name) { reply.ok(); return; } } let base_offset = entries.len(); for (i, (inode, kind, name)) in child_entries.iter().enumerate() { let entry_offset = base_offset + i; if entry_offset < offset as usize { continue; } if reply.add(*inode, (entry_offset + 1) as i64, *kind, name) { break; } } reply.ok(); } else { trace!(ino, offset, "directory not found"); reply.error(libc::ENOENT); } } #[instrument(level = "debug", skip(self, reply))] fn open(&mut self, _req: &Request, ino: u64, flags: i32, reply: ReplyOpen) { let write_flags = libc::O_WRONLY | libc::O_RDWR | libc::O_APPEND | libc::O_TRUNC; if flags & write_flags != 0 { trace!(ino, flags, "write flags detected"); reply.error(libc::EROFS); return; } let tree = self.tree.read(); if tree.get(ino).is_some() { trace!(ino, "inode found"); reply.opened(0, 0); } else { trace!(ino, "inode not found"); reply.error(libc::ENOENT); } } #[instrument(level = "debug", skip(self, reply))] fn read( &mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, size: u32, _flags: i32, _lock_owner: Option, reply: ReplyData, ) { let file_id = { let tree = self.tree.read(); if let Some(VirtualNode::File(file)) = tree.get(ino) { trace!(ino, "file found in tree"); file.file_id } else { trace!(ino, "file not found"); reply.error(libc::ENOENT); return; } }; let handle = self.runtime_handle.clone(); if let Some(ref overlay) = self.overlay_reader { let overlay = overlay.clone(); let result = std::thread::scope(|_| { handle.block_on(async { tokio::time::timeout( Duration::from_secs(30), overlay.read(file_id, offset as u64, size), ) .await }) }); match result { Ok(Ok(data)) => { trace!( ino, offset, size_bytes = size, bytes_read = data.len(), "overlay read successful" ); reply.data(&data); } Ok(Err(e)) => { let errno = match &e { OverlayError::NotFound(_) => libc::ENOENT, OverlayError::Database(_) => libc::EIO, OverlayError::Handler(_) => libc::EIO, OverlayError::Cas(_) => libc::EIO, OverlayError::NoHandler(_) => libc::EIO, }; warn!(ino, offset, size_bytes = size, error = %e, "overlay read failed"); reply.error(errno); } Err(_timeout) => { warn!( ino, offset, size_bytes = size, "overlay read timed out after 30s" ); reply.error(libc::EIO); } } } else { let Some(reader) = &self.reader else { trace!(ino, "no reader available"); reply.data(&[]); return; }; let reader = reader.clone(); let result = std::thread::scope(|_| { handle.block_on(async { tokio::time::timeout( Duration::from_secs(30), reader.read(file_id, offset as u64, size), ) .await }) }); match result { Ok(Ok(data)) => { trace!( ino, offset, size_bytes = size, bytes_read = data.len(), "read successful" ); reply.data(&data); } Ok(Err(e)) => { warn!(ino, offset, size_bytes = size, error = %e, "read failed"); reply.error(libc::EIO); } Err(_timeout) => { warn!(ino, offset, size_bytes = size, "read timed out after 30s"); reply.error(libc::EIO); } } } } #[instrument(level = "debug", skip(self, reply))] fn release( &mut self, _req: &Request, ino: u64, _fh: u64, _flags: i32, _lock_owner: Option, _flush: bool, reply: fuser::ReplyEmpty, ) { trace!(ino, "releasing file handle"); reply.ok(); } fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) { debug!("readlink(ino={})", ino); if SearchOps::is_search_inode(ino) { if let Some(ref search_ops) = self.search_ops { search_ops.readlink(ino, reply); return; } } reply.error(libc::EINVAL); } fn write( &mut self, _req: &Request, _ino: u64, _fh: u64, _offset: i64, _data: &[u8], _write_flags: u32, _flags: i32, _lock_owner: Option, reply: fuser::ReplyWrite, ) { reply.error(libc::EROFS); } fn mkdir( &mut self, _req: &Request, parent: u64, name: &OsStr, _mode: u32, _umask: u32, reply: ReplyEntry, ) { let path = match self.resolve_path(parent, name) { Some(p) => p, None => { reply.error(libc::ENOENT); return; } }; let mut tree = self.tree.write(); match tree.mkdir(&path) { Ok(inode) => { if let Some(ref db) = self.db { if let Err(e) = db.insert_directory(&path) { warn!(error = %e, "failed to persist directory to database"); } } let attr = FileAttr { ino: inode, size: 0, blocks: 0, atime: SystemTime::now(), mtime: SystemTime::now(), ctime: SystemTime::now(), crtime: SystemTime::now(), kind: FileType::Directory, perm: 0o755, nlink: 2, uid: self.uid, gid: self.gid, rdev: 0, blksize: BLOCK_SIZE, flags: 0, }; debug!(path = %path.as_str(), inode, "mkdir successful"); reply.entry(&TTL, &attr, 0); } Err(RenameError::TargetExists) => reply.error(libc::EEXIST), Err(RenameError::ParentNotFound) => reply.error(libc::ENOENT), Err(_) => reply.error(libc::EIO), } } fn unlink(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) { let path = match self.resolve_path(parent, name) { Some(p) => p, None => { reply.error(libc::ENOENT); return; } }; let (file_id, is_dir) = { let tree = self.tree.read(); match tree.get_by_path(&path) { Some(VirtualNode::File(f)) => (Some(f.file_id), false), Some(VirtualNode::Directory(_)) => (None, true), None => { reply.error(libc::ENOENT); return; } } }; if is_dir { reply.error(libc::EISDIR); return; } let trash_path = VirtualPath::new(format!("/.trash{}", path.as_str())); { let mut tree = self.tree.write(); tree.ensure_trash_dir(); let trash_parent = std::path::Path::new(trash_path.as_str()) .parent() .map(|p| VirtualPath::new(p.to_string_lossy().into_owned())) .unwrap_or_else(|| VirtualPath::new("/.trash")); if let Err(e) = tree.mkdir_p(&trash_parent) { if !matches!(e, RenameError::TargetExists) { warn!(error = ?e, "failed to create trash parent directories"); reply.error(libc::EIO); return; } } if let Err(e) = tree.rename_file(&path, &trash_path) { match e { RenameError::SourceNotFound => reply.error(libc::ENOENT), RenameError::TargetExists => reply.error(libc::EEXIST), _ => reply.error(libc::EIO), } return; } } if let (Some(ref db), Some(id)) = (&self.db, file_id) { if let Err(e) = db.update_virtual_path(id, &trash_path) { warn!(error = %e, "failed to update virtual path in database"); } if let Err(e) = db.mark_trashed(id, &path) { warn!(error = %e, "failed to mark file as trashed in database"); } } debug!(path = %path.as_str(), trash = %trash_path.as_str(), "file moved to trash"); reply.ok(); } fn rmdir(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) { let path = match self.resolve_path(parent, name) { Some(p) => p, None => { reply.error(libc::ENOENT); return; } }; if VirtualTree::is_trash_path(&path) { reply.error(libc::EPERM); return; } { let mut tree = self.tree.write(); match tree.remove_directory(&path) { Ok(()) => {} Err(RemoveError::NotFound) => { reply.error(libc::ENOENT); return; } Err(RemoveError::NotEmpty) => { reply.error(libc::ENOTEMPTY); return; } Err(RemoveError::NotDirectory) => { reply.error(libc::ENOTDIR); return; } } } if let Some(ref db) = self.db { if let Err(e) = db.delete_directory(&path) { warn!(error = %e, "failed to delete directory from database"); } } debug!(path = %path.as_str(), "directory removed"); reply.ok(); } fn rename( &mut self, _req: &Request, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr, _flags: u32, reply: fuser::ReplyEmpty, ) { let old_path = match self.resolve_path(parent, name) { Some(p) => p, None => { reply.error(libc::ENOENT); return; } }; let new_path = match self.resolve_path(newparent, newname) { Some(p) => p, None => { reply.error(libc::ENOENT); return; } }; if old_path.as_str() == new_path.as_str() { reply.ok(); return; } let is_dir = { let tree = self.tree.read(); tree.get_by_path(&old_path) .map(|n| n.is_dir()) .unwrap_or(false) }; let result = if is_dir { let mut tree = self.tree.write(); match tree.rename_directory(&old_path, &new_path) { Ok(count) => { if let Some(ref db) = self.db { let old_prefix = if old_path.as_str().ends_with('/') { old_path.as_str().to_string() } else { format!("{}/", old_path.as_str()) }; let new_prefix = if new_path.as_str().ends_with('/') { new_path.as_str().to_string() } else { format!("{}/", new_path.as_str()) }; if let Err(e) = db.rename_directory(&old_prefix, &new_prefix) { warn!(error = %e, "failed to persist file path rename to database"); } if let Err(e) = db.rename_directories(&old_prefix, &new_prefix) { warn!(error = %e, "failed to persist directory rename to database"); } } debug!(old = %old_path.as_str(), new = %new_path.as_str(), count, "directory renamed"); Ok(()) } Err(e) => Err(e), } } else { let file_id = { let tree = self.tree.read(); match tree.get_by_path(&old_path) { Some(VirtualNode::File(f)) => Some(f.file_id), _ => None, } }; let mut tree = self.tree.write(); match tree.rename_file(&old_path, &new_path) { Ok(()) => { if let (Some(ref db), Some(id)) = (&self.db, file_id) { if let Err(e) = db.update_virtual_path(id, &new_path) { warn!(error = %e, "failed to persist file rename to database"); } let was_in_trash = VirtualTree::is_trash_path(&old_path); let now_in_trash = VirtualTree::is_trash_path(&new_path); if was_in_trash && !now_in_trash { if let Err(e) = db.unmark_trashed(id) { warn!(error = %e, "failed to unmark trashed after restore"); } debug!(path = %new_path.as_str(), "file restored from trash"); } } debug!(old = %old_path.as_str(), new = %new_path.as_str(), "file renamed"); Ok(()) } Err(e) => Err(e), } }; match result { Ok(()) => reply.ok(), Err(RenameError::SourceNotFound) => reply.error(libc::ENOENT), Err(RenameError::TargetExists) => reply.error(libc::EEXIST), Err(RenameError::ParentNotFound) => reply.error(libc::ENOENT), Err(RenameError::IsDirectory) => reply.error(libc::EISDIR), Err(RenameError::NotDirectory) => reply.error(libc::ENOTDIR), } } fn create( &mut self, _req: &Request, _parent: u64, _name: &OsStr, _mode: u32, _umask: u32, _flags: i32, reply: fuser::ReplyCreate, ) { reply.error(libc::EROFS); } fn setattr( &mut self, _req: &Request, _ino: u64, _mode: Option, _uid: Option, _gid: Option, _size: Option, _atime: Option, _mtime: Option, _ctime: Option, _fh: Option, _crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option, reply: ReplyAttr, ) { reply.error(libc::EROFS); } fn symlink( &mut self, _req: &Request, _parent: u64, _name: &OsStr, _link: &Path, reply: ReplyEntry, ) { reply.error(libc::EROFS); } fn link( &mut self, _req: &Request, _ino: u64, _newparent: u64, _newname: &OsStr, reply: ReplyEntry, ) { reply.error(libc::EROFS); } fn mknod( &mut self, _req: &Request, _parent: u64, _name: &OsStr, _mode: u32, _umask: u32, _rdev: u32, reply: ReplyEntry, ) { reply.error(libc::EROFS); } } #[cfg(test)] mod tests { use super::*; use musicfs_cache::TreeBuilder; use musicfs_core::{FileId, FileMeta, OriginId, RealPath, VirtualPath}; use std::path::PathBuf; fn make_file_meta(id: i64, vpath: &str, size: u64) -> FileMeta { FileMeta { id: FileId(id), virtual_path: VirtualPath::new(vpath), real_path: RealPath { origin_id: OriginId::from("test"), path: PathBuf::from("/test"), }, size, mtime: SystemTime::now(), content_hash: None, audio: None, } } #[test] fn test_tree_integration() { let runtime = tokio::runtime::Runtime::new().unwrap(); let handle = runtime.handle().clone(); let mut builder = TreeBuilder::new(); builder.add_file(&make_file_meta(1, "/Artist/Album/Track.flac", 30_000_000)); let tree = Arc::new(RwLock::new(builder.build())); let _fs = MusicFs::new(tree.clone(), handle); let tree_read = tree.read(); assert!(tree_read.get(ROOT_INODE).is_some()); assert!(tree_read .get_by_path(&VirtualPath::new("/Artist")) .is_some()); } }