diff --git a/musicfs/Cargo.lock b/musicfs/Cargo.lock index 4e1fac1..b6a2bc5 100644 --- a/musicfs/Cargo.lock +++ b/musicfs/Cargo.lock @@ -406,6 +406,7 @@ version = "0.1.0" dependencies = [ "fuser", "libc", + "musicfs-cache", "musicfs-core", "tokio", "tracing", diff --git a/musicfs/crates/musicfs-cache/src/lib.rs b/musicfs/crates/musicfs-cache/src/lib.rs index ae4da5f..4456134 100644 --- a/musicfs/crates/musicfs-cache/src/lib.rs +++ b/musicfs/crates/musicfs-cache/src/lib.rs @@ -1,5 +1,9 @@ mod db; mod metadata; +mod tree; pub use db::Database; pub use metadata::MetadataCache; +pub use tree::{ + DirNode, FileNode, Inode, RefreshPolicy, TreeBuilder, VirtualNode, VirtualTree, ROOT_INODE, +}; diff --git a/musicfs/crates/musicfs-cache/src/tree.rs b/musicfs/crates/musicfs-cache/src/tree.rs new file mode 100644 index 0000000..c9cb6c6 --- /dev/null +++ b/musicfs/crates/musicfs-cache/src/tree.rs @@ -0,0 +1,444 @@ +use musicfs_core::{FileId, FileMeta, VirtualPath}; +use std::collections::{BTreeMap, HashMap}; +use std::ffi::{OsStr, OsString}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::RwLock; +use std::time::{Duration, SystemTime}; + +pub type Inode = u64; +pub const ROOT_INODE: Inode = 1; + +#[derive(Debug)] +pub enum VirtualNode { + Directory(DirNode), + File(FileNode), +} + +impl VirtualNode { + pub fn inode(&self) -> Inode { + match self { + VirtualNode::Directory(d) => d.inode, + VirtualNode::File(f) => f.inode, + } + } + + pub fn name(&self) -> &OsStr { + match self { + VirtualNode::Directory(d) => &d.name, + VirtualNode::File(f) => &f.name, + } + } + + pub fn is_dir(&self) -> bool { + matches!(self, VirtualNode::Directory(_)) + } +} + +#[derive(Debug)] +pub struct DirNode { + pub inode: Inode, + pub parent: Inode, + pub name: OsString, + pub children: BTreeMap, + pub mtime: SystemTime, +} + +#[derive(Debug)] +pub struct FileNode { + pub inode: Inode, + pub name: OsString, + pub file_id: FileId, + pub size: u64, + pub mtime: SystemTime, +} + +#[derive(Debug, Clone)] +pub struct RefreshPolicy { + pub ttl: Duration, + pub refresh_on_access: bool, + pub background_interval: Option, +} + +impl Default for RefreshPolicy { + fn default() -> Self { + Self { + ttl: Duration::from_secs(300), + refresh_on_access: false, + background_interval: None, + } + } +} + +pub struct VirtualTree { + nodes: HashMap, + path_to_inode: HashMap, + next_inode: AtomicU64, + last_refresh: RwLock, + refresh_policy: RefreshPolicy, +} + +impl VirtualTree { + pub fn new() -> Self { + Self::with_policy(RefreshPolicy::default()) + } + + pub fn with_policy(policy: RefreshPolicy) -> Self { + let mut tree = Self { + nodes: HashMap::new(), + path_to_inode: HashMap::new(), + next_inode: AtomicU64::new(ROOT_INODE + 1), + last_refresh: RwLock::new(SystemTime::now()), + refresh_policy: policy, + }; + + tree.nodes.insert( + ROOT_INODE, + VirtualNode::Directory(DirNode { + inode: ROOT_INODE, + parent: ROOT_INODE, + name: OsString::from(""), + children: BTreeMap::new(), + mtime: SystemTime::now(), + }), + ); + tree.path_to_inode + .insert(VirtualPath::new("/"), ROOT_INODE); + + tree + } + + fn alloc_inode(&self) -> Inode { + self.next_inode.fetch_add(1, Ordering::SeqCst) + } + + pub fn get(&self, inode: Inode) -> Option<&VirtualNode> { + self.nodes.get(&inode) + } + + pub fn get_by_path(&self, path: &VirtualPath) -> Option<&VirtualNode> { + self.path_to_inode + .get(path) + .and_then(|ino| self.nodes.get(ino)) + } + + pub fn lookup(&self, parent_inode: Inode, name: &OsStr) -> Option { + if let Some(VirtualNode::Directory(dir)) = self.nodes.get(&parent_inode) { + dir.children.get(name).copied() + } else { + None + } + } + + pub fn readdir(&self, inode: Inode) -> Option> { + if let Some(VirtualNode::Directory(dir)) = self.nodes.get(&inode) { + Some( + dir.children + .iter() + .map(|(name, &ino)| { + let is_dir = self.nodes.get(&ino).map(|n| n.is_dir()).unwrap_or(false); + (name.clone(), ino, is_dir) + }) + .collect(), + ) + } else { + None + } + } + + pub fn get_parent(&self, inode: Inode) -> Option { + match self.nodes.get(&inode) { + Some(VirtualNode::Directory(dir)) => Some(dir.parent), + Some(VirtualNode::File(_)) => self.find_parent_by_path_lookup(inode), + None => None, + } + } + + fn find_parent_by_path_lookup(&self, inode: Inode) -> Option { + for (path, &ino) in &self.path_to_inode { + if ino == inode { + return std::path::Path::new(path.as_str()) + .parent() + .and_then(|p| { + self.path_to_inode + .get(&VirtualPath::new(p.to_string_lossy().into_owned())) + .copied() + }); + } + } + None + } + + pub fn insert_file(&mut self, meta: &FileMeta) -> Inode { + let path = &meta.virtual_path; + + let parent_inode = self.ensure_parents(path); + + let inode = self.alloc_inode(); + let name = std::path::Path::new(path.as_str()) + .file_name() + .unwrap_or_default() + .to_os_string(); + + let file_node = FileNode { + inode, + name: name.clone(), + file_id: meta.id, + size: meta.size, + mtime: meta.mtime, + }; + + self.nodes.insert(inode, VirtualNode::File(file_node)); + self.path_to_inode.insert(path.clone(), inode); + + if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&parent_inode) { + dir.children.insert(name, inode); + } + + inode + } + + fn ensure_parents(&mut self, path: &VirtualPath) -> Inode { + let path_str = path.as_str(); + let components: Vec<&str> = path_str + .trim_start_matches('/') + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + + if components.len() <= 1 { + return ROOT_INODE; + } + + let mut current_inode = ROOT_INODE; + let mut current_path = String::from("/"); + + for component in &components[..components.len() - 1] { + current_path.push_str(component); + + let vpath = VirtualPath::new(¤t_path); + + if let Some(&existing) = self.path_to_inode.get(&vpath) { + current_inode = existing; + } else { + let new_inode = self.alloc_inode(); + let name = OsString::from(*component); + + let dir_node = DirNode { + inode: new_inode, + parent: current_inode, + name: name.clone(), + children: BTreeMap::new(), + mtime: SystemTime::now(), + }; + + self.nodes + .insert(new_inode, VirtualNode::Directory(dir_node)); + self.path_to_inode.insert(vpath, new_inode); + + if let Some(VirtualNode::Directory(parent)) = self.nodes.get_mut(¤t_inode) { + parent.children.insert(name, new_inode); + } + + current_inode = new_inode; + } + + current_path.push('/'); + } + + current_inode + } + + pub fn remove_file(&mut self, path: &VirtualPath) -> Option { + let inode = self.path_to_inode.remove(path)?; + + if let Some(VirtualNode::File(file)) = self.nodes.remove(&inode) { + let parent_path = std::path::Path::new(path.as_str()) + .parent() + .map(|p| VirtualPath::new(p.to_string_lossy().into_owned())) + .unwrap_or_else(|| VirtualPath::new("/")); + + if let Some(&parent_inode) = self.path_to_inode.get(&parent_path) { + if let Some(VirtualNode::Directory(dir)) = self.nodes.get_mut(&parent_inode) { + dir.children.remove(&file.name); + } + } + + Some(file.file_id) + } else { + None + } + } + + pub fn file_count(&self) -> usize { + self.nodes + .values() + .filter(|n| matches!(n, VirtualNode::File(_))) + .count() + } + + pub fn dir_count(&self) -> usize { + self.nodes + .values() + .filter(|n| matches!(n, VirtualNode::Directory(_))) + .count() + } + + pub fn needs_refresh(&self) -> bool { + let last = *self.last_refresh.read().unwrap(); + last.elapsed().unwrap_or(Duration::MAX) > self.refresh_policy.ttl + } + + pub fn force_refresh(&mut self) { + self.nodes.retain(|&ino, _| ino == ROOT_INODE); + self.path_to_inode.retain(|p, _| p.as_str() == "/"); + + if let Some(VirtualNode::Directory(root)) = self.nodes.get_mut(&ROOT_INODE) { + root.children.clear(); + } + + *self.last_refresh.write().unwrap() = SystemTime::now(); + } + + pub fn mark_refreshed(&self) { + *self.last_refresh.write().unwrap() = SystemTime::now(); + } + + pub fn refresh_policy(&self) -> &RefreshPolicy { + &self.refresh_policy + } +} + +impl Default for VirtualTree { + fn default() -> Self { + Self::new() + } +} + +pub struct TreeBuilder { + tree: VirtualTree, +} + +impl TreeBuilder { + pub fn new() -> Self { + Self { + tree: VirtualTree::new(), + } + } + + pub fn with_policy(policy: RefreshPolicy) -> Self { + Self { + tree: VirtualTree::with_policy(policy), + } + } + + pub fn add_file(&mut self, meta: &FileMeta) { + self.tree.insert_file(meta); + } + + pub fn build(self) -> VirtualTree { + self.tree + } +} + +impl Default for TreeBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use musicfs_core::{OriginId, RealPath}; + use std::path::PathBuf; + + fn make_file_meta(id: i64, vpath: &str) -> FileMeta { + FileMeta { + id: FileId(id), + virtual_path: VirtualPath::new(vpath), + real_path: RealPath { + origin_id: OriginId::from("test"), + path: PathBuf::from("/test"), + }, + size: 1000, + mtime: SystemTime::now(), + content_hash: None, + audio: None, + } + } + + #[test] + fn test_tree_creation() { + let tree = VirtualTree::new(); + assert!(tree.get(ROOT_INODE).is_some()); + } + + #[test] + fn test_insert_file() { + let mut tree = VirtualTree::new(); + let meta = make_file_meta(1, "/Artist/Album/Track.flac"); + tree.insert_file(&meta); + + assert!(tree.get_by_path(&VirtualPath::new("/Artist")).is_some()); + assert!(tree + .get_by_path(&VirtualPath::new("/Artist/Album")) + .is_some()); + assert!(tree + .get_by_path(&VirtualPath::new("/Artist/Album/Track.flac")) + .is_some()); + } + + #[test] + fn test_readdir() { + let mut tree = VirtualTree::new(); + tree.insert_file(&make_file_meta(1, "/Artist/Album/Track1.flac")); + tree.insert_file(&make_file_meta(2, "/Artist/Album/Track2.flac")); + + let root_children = tree.readdir(ROOT_INODE).unwrap(); + assert_eq!(root_children.len(), 1); + assert_eq!(root_children[0].0, "Artist"); + } + + #[test] + fn test_lookup() { + let mut tree = VirtualTree::new(); + tree.insert_file(&make_file_meta(1, "/Artist/Album/Track.flac")); + + let artist_inode = tree.lookup(ROOT_INODE, OsStr::new("Artist")).unwrap(); + assert!(tree.lookup(artist_inode, OsStr::new("Album")).is_some()); + } + + #[test] + fn test_file_and_dir_count() { + let mut tree = VirtualTree::new(); + tree.insert_file(&make_file_meta(1, "/A/B/Track1.flac")); + tree.insert_file(&make_file_meta(2, "/A/B/Track2.flac")); + tree.insert_file(&make_file_meta(3, "/A/C/Track3.flac")); + + assert_eq!(tree.file_count(), 3); + assert_eq!(tree.dir_count(), 4); + } + + #[test] + fn test_remove_file() { + let mut tree = VirtualTree::new(); + let path = VirtualPath::new("/Artist/Album/Track.flac"); + tree.insert_file(&make_file_meta(1, path.as_str())); + + assert!(tree.get_by_path(&path).is_some()); + + let removed_id = tree.remove_file(&path); + assert_eq!(removed_id, Some(FileId(1))); + assert!(tree.get_by_path(&path).is_none()); + } + + #[test] + fn test_tree_builder() { + let mut builder = TreeBuilder::new(); + builder.add_file(&make_file_meta(1, "/A/Track1.flac")); + builder.add_file(&make_file_meta(2, "/A/Track2.flac")); + + let tree = builder.build(); + assert_eq!(tree.file_count(), 2); + } +} diff --git a/musicfs/crates/musicfs-core/src/lib.rs b/musicfs/crates/musicfs-core/src/lib.rs index 02d5d5f..cb90d46 100644 --- a/musicfs/crates/musicfs-core/src/lib.rs +++ b/musicfs/crates/musicfs-core/src/lib.rs @@ -1,7 +1,9 @@ pub mod error; pub mod events; +pub mod resolver; pub mod types; pub use error::{Error, Result}; pub use events::{Event, EventBus}; +pub use resolver::{PathResolver, PathTemplate}; pub use types::*; diff --git a/musicfs/crates/musicfs-core/src/resolver.rs b/musicfs/crates/musicfs-core/src/resolver.rs new file mode 100644 index 0000000..b6c5dd6 --- /dev/null +++ b/musicfs/crates/musicfs-core/src/resolver.rs @@ -0,0 +1,174 @@ +use crate::{AudioMeta, VirtualPath}; + +#[derive(Debug, Clone)] +pub struct PathTemplate { + pub pattern: String, + pub fallback_artist: String, + pub fallback_album: String, + pub fallback_title: String, + pub fallback_year: String, +} + +impl Default for PathTemplate { + fn default() -> Self { + Self { + pattern: "$artist/$album ($year) [$format_upper]/$track - $title.$format".to_string(), + fallback_artist: "Unknown Artist".to_string(), + fallback_album: "Unknown Album".to_string(), + fallback_title: "Unknown Track".to_string(), + fallback_year: "Unknown".to_string(), + } + } +} + +pub struct PathResolver { + template: PathTemplate, +} + +impl PathResolver { + pub fn new(template: PathTemplate) -> Self { + Self { template } + } + + pub fn resolve(&self, meta: &AudioMeta, extension: &str) -> VirtualPath { + let artist = meta + .artist + .as_deref() + .unwrap_or(&self.template.fallback_artist); + let album = meta + .album + .as_deref() + .unwrap_or(&self.template.fallback_album); + let title = meta + .title + .as_deref() + .unwrap_or(&self.template.fallback_title); + let year = meta + .year + .map(|y| y.to_string()) + .unwrap_or_else(|| self.template.fallback_year.clone()); + let track = meta.track.unwrap_or(0); + let disc = meta.disc.unwrap_or(1); + let genre = meta.genre.as_deref().unwrap_or("Unknown"); + let format = extension.to_lowercase(); + let format_upper = extension.to_uppercase(); + + let artist = sanitize_path_component(artist); + let album = sanitize_path_component(album); + let title = sanitize_path_component(title); + let genre = sanitize_path_component(genre); + + let path = self + .template + .pattern + .replace("$artist", &artist) + .replace("$album", &album) + .replace("$title", &title) + .replace("$track", &format!("{:02}", track)) + .replace("$disc", &disc.to_string()) + .replace("$year", &year) + .replace("$genre", &genre) + .replace("$format_upper", &format_upper) + .replace("$format", &format); + + VirtualPath::new(path) + } +} + +impl Default for PathResolver { + fn default() -> Self { + Self::new(PathTemplate::default()) + } +} + +fn sanitize_path_component(s: &str) -> String { + s.chars() + .map(|c| match c { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_', + c => c, + }) + .collect::() + .trim() + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::AudioFormat; + + #[test] + fn test_resolve_complete_metadata() { + let resolver = PathResolver::default(); + let meta = AudioMeta { + artist: Some("Metallica".to_string()), + album: Some("Master of Puppets".to_string()), + title: Some("Battery".to_string()), + track: Some(1), + year: Some(1986), + format: AudioFormat::Flac, + ..Default::default() + }; + + let path = resolver.resolve(&meta, "flac"); + assert_eq!( + path.as_str(), + "Metallica/Master of Puppets (1986) [FLAC]/01 - Battery.flac" + ); + } + + #[test] + fn test_resolve_missing_album() { + let resolver = PathResolver::default(); + let meta = AudioMeta { + artist: Some("Artist".to_string()), + title: Some("Track".to_string()), + track: Some(5), + ..Default::default() + }; + + let path = resolver.resolve(&meta, "mp3"); + assert_eq!( + path.as_str(), + "Artist/Unknown Album (Unknown) [MP3]/05 - Track.mp3" + ); + } + + #[test] + fn test_sanitize_special_chars() { + let resolver = PathResolver::default(); + let meta = AudioMeta { + artist: Some("AC/DC".to_string()), + album: Some("Who Made Who?".to_string()), + title: Some("Test:Track".to_string()), + track: Some(1), + year: Some(1986), + ..Default::default() + }; + + let path = resolver.resolve(&meta, "flac"); + assert!(!path.as_str().contains(':')); + assert!(!path.as_str().contains('?')); + assert!(path.as_str().contains("AC_DC")); + } + + #[test] + fn test_custom_template() { + let template = PathTemplate { + pattern: "$genre/$artist - $album/$track $title.$format".to_string(), + ..Default::default() + }; + let resolver = PathResolver::new(template); + let meta = AudioMeta { + artist: Some("Artist".to_string()), + album: Some("Album".to_string()), + title: Some("Song".to_string()), + genre: Some("Rock".to_string()), + track: Some(3), + ..Default::default() + }; + + let path = resolver.resolve(&meta, "flac"); + assert_eq!(path.as_str(), "Rock/Artist - Album/03 Song.flac"); + } +} diff --git a/musicfs/crates/musicfs-fuse/Cargo.toml b/musicfs/crates/musicfs-fuse/Cargo.toml index 7ac2d2f..a5948fe 100644 --- a/musicfs/crates/musicfs-fuse/Cargo.toml +++ b/musicfs/crates/musicfs-fuse/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] musicfs-core = { path = "../musicfs-core" } +musicfs-cache = { path = "../musicfs-cache" } fuser.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/musicfs/crates/musicfs-fuse/src/filesystem.rs b/musicfs/crates/musicfs-fuse/src/filesystem.rs index 89c69ae..066eaa9 100644 --- a/musicfs/crates/musicfs-fuse/src/filesystem.rs +++ b/musicfs/crates/musicfs-fuse/src/filesystem.rs @@ -1,23 +1,28 @@ use fuser::{ FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen, - Request, FUSE_ROOT_ID, + Request, }; -use musicfs_core::{Error, Result}; +use musicfs_cache::{VirtualNode, VirtualTree, ROOT_INODE}; +use musicfs_core::Result; use std::ffi::OsStr; use std::path::Path; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime}; use tracing::{debug, info}; const TTL: Duration = Duration::from_secs(1); +const BLOCK_SIZE: u32 = 512; pub struct MusicFs { + tree: Arc>, uid: u32, gid: u32, } impl MusicFs { - pub fn new() -> Self { + pub fn new(tree: Arc>) -> Self { Self { + tree, uid: unsafe { libc::getuid() }, gid: unsafe { libc::getgid() }, } @@ -33,38 +38,51 @@ impl MusicFs { fuser::MountOption::AllowOther, ]; - fuser::mount2(self, mountpoint, &options).map_err(Error::Io)?; + fuser::mount2(self, mountpoint, &options).map_err(musicfs_core::Error::Io)?; Ok(()) } - fn root_attr(&self) -> FileAttr { - FileAttr { - ino: FUSE_ROOT_ID, - size: 0, - blocks: 0, - atime: UNIX_EPOCH, - mtime: UNIX_EPOCH, - ctime: UNIX_EPOCH, - crtime: UNIX_EPOCH, - kind: FileType::Directory, - perm: 0o755, - nlink: 2, - uid: self.uid, - gid: self.gid, - rdev: 0, - blksize: 512, - flags: 0, + 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 Default for MusicFs { - fn default() -> Self { - Self::new() - } -} - impl Filesystem for MusicFs { fn init( &mut self, @@ -81,14 +99,28 @@ impl Filesystem for MusicFs { fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { debug!("lookup(parent={}, name={:?})", parent, name); + + let tree = self.tree.read().unwrap(); + + if let Some(inode) = tree.lookup(parent, name) { + if let Some(node) = tree.get(inode) { + let attr = self.node_to_attr(node); + reply.entry(&TTL, &attr, 0); + return; + } + } + reply.error(libc::ENOENT); } fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { debug!("getattr(ino={})", ino); - if ino == FUSE_ROOT_ID { - reply.attr(&TTL, &self.root_attr()); + let tree = self.tree.read().unwrap(); + + if let Some(node) = tree.get(ino) { + let attr = self.node_to_attr(node); + reply.attr(&TTL, &attr); } else { reply.error(libc::ENOENT); } @@ -104,13 +136,46 @@ impl Filesystem for MusicFs { ) { debug!("readdir(ino={}, offset={})", ino, offset); - if ino == FUSE_ROOT_ID { - if offset == 0 { - let _ = reply.add(FUSE_ROOT_ID, 1, FileType::Directory, "."); + let tree = self.tree.read().unwrap(); + + if let Some(children) = tree.readdir(ino) { + 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; + } } - if offset <= 1 { - let _ = reply.add(FUSE_ROOT_ID, 2, FileType::Directory, ".."); + + 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 { reply.error(libc::ENOENT); @@ -126,7 +191,13 @@ impl Filesystem for MusicFs { return; } - reply.error(libc::ENOENT); + let tree = self.tree.read().unwrap(); + + if tree.get(ino).is_some() { + reply.opened(0, 0); + } else { + reply.error(libc::ENOENT); + } } fn read( @@ -141,7 +212,14 @@ impl Filesystem for MusicFs { reply: ReplyData, ) { debug!("read(ino={}, offset={}, size={})", ino, offset, size); - reply.error(libc::ENOENT); + + let tree = self.tree.read().unwrap(); + + if let Some(VirtualNode::File(_file)) = tree.get(ino) { + reply.data(&[]); + } else { + reply.error(libc::ENOENT); + } } fn release( @@ -275,3 +353,39 @@ impl Filesystem for MusicFs { 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 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()); + + let tree_read = tree.read().unwrap(); + assert!(tree_read.get(ROOT_INODE).is_some()); + assert!(tree_read.get_by_path(&VirtualPath::new("/Artist")).is_some()); + } +}