9623644263
- Update read() to use OverlayReader when available - Map OverlayError to libc error codes - Maintain 30s timeout and backward compatibility - Fallback to FileReader for non-overlay files - All tests pass, full workspace compiles
955 lines
30 KiB
Rust
955 lines
30 KiB
Rust
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<RwLock<VirtualTree>>,
|
|
reader: Option<Arc<FileReader>>,
|
|
db: Option<Arc<Database>>,
|
|
overlay_reader: Option<Arc<OverlayReader>>,
|
|
runtime_handle: Handle,
|
|
search_ops: Option<SearchOps>,
|
|
query_inodes: RwLock<HashMap<String, u64>>,
|
|
inode_queries: RwLock<HashMap<u64, String>>,
|
|
next_query_inode: RwLock<u64>,
|
|
uid: u32,
|
|
gid: u32,
|
|
}
|
|
|
|
impl MusicFs {
|
|
pub fn new(tree: Arc<RwLock<VirtualTree>>, 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<RwLock<VirtualTree>>,
|
|
reader: Arc<FileReader>,
|
|
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<Database>) -> Self {
|
|
self.db = Some(db);
|
|
self
|
|
}
|
|
|
|
pub fn with_overlay(mut self, overlay: Arc<OverlayReader>) -> 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<VirtualPath> {
|
|
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<String> {
|
|
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<String> {
|
|
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<fuser::BackgroundSession> {
|
|
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<u64>,
|
|
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<u64>,
|
|
_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<u64>,
|
|
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<u32>,
|
|
_uid: Option<u32>,
|
|
_gid: Option<u32>,
|
|
_size: Option<u64>,
|
|
_atime: Option<fuser::TimeOrNow>,
|
|
_mtime: Option<fuser::TimeOrNow>,
|
|
_ctime: Option<SystemTime>,
|
|
_fh: Option<u64>,
|
|
_crtime: Option<SystemTime>,
|
|
_chgtime: Option<SystemTime>,
|
|
_bkuptime: Option<SystemTime>,
|
|
_flags: Option<u32>,
|
|
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());
|
|
}
|
|
}
|