Implement Week 3 virtual tree with path resolver and FUSE integration

This commit is contained in:
Alexander
2026-05-12 18:25:24 +02:00
parent d664439746
commit d9e5e06166
7 changed files with 777 additions and 37 deletions
+1
View File
@@ -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
+151 -37
View File
@@ -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<RwLock<VirtualTree>>,
uid: u32,
gid: u32,
}
impl MusicFs {
pub fn new() -> Self {
pub fn new(tree: Arc<RwLock<VirtualTree>>) -> 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());
}
}