use std::path::{Path, PathBuf}; use std::ffi::CStr; use std::io; use std::time::Duration; use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; use anyhow::Result; use fuse_backend_rs::api::filesystem::{FileSystem, Entry, DirEntry, Context}; use fuse_backend_rs::abi::fuse_abi::{FsOptions, OpenOptions, statvfs64}; use libc::{stat as stat64, DT_DIR, DT_REG}; use crate::fuse::backend::BackendType; pub struct MarkBaseFs { user_id: String, db_path: PathBuf, backend: BackendType, } struct QueryNodeResult { node_id: String, label: String, node_type: String, file_size: Option, parent_id: Option, created_at: Option, updated_at: Option, } impl MarkBaseFs { pub fn new(user_id: String, db_path: PathBuf, backend: BackendType) -> Self { MarkBaseFs { user_id, db_path, backend, } } pub fn get_user_id(&self) -> &str { &self.user_id } pub fn get_backend(&self) -> &BackendType { &self.backend } pub fn get_db_path(&self) -> &Path { &self.db_path } pub fn mount(&self, mount_path: &Path) -> Result<()> { println!("=== Mounting MarkBase FUSE ==="); println!("User: {}", self.user_id); println!("Database: {}", self.db_path.display()); println!("Backend: {}", self.backend.name()); println!("Mount path: {}", mount_path.display()); Ok(()) } pub fn uuid_to_ino(uuid: &str) -> u64 { let bytes = uuid.as_bytes(); if bytes.len() >= 8 { u64::from_be_bytes([ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], ]) } else { 0 } } pub fn ino_to_uuid(ino: u64) -> String { let bytes = ino.to_be_bytes(); format!("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]) } fn query_node(&self, uuid: &str) -> io::Result { use rusqlite::Connection; let conn = Connection::open(&self.db_path) .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; conn.query_row( "SELECT node_id, label, node_type, file_size, parent_id, created_at, updated_at FROM file_nodes WHERE node_id = ?", [uuid], |row| { Ok(QueryNodeResult { node_id: row.get::<_, String>(0)?, label: row.get::<_, String>(1)?, node_type: row.get::<_, String>(2)?, file_size: row.get::<_, Option>(3)?, parent_id: row.get::<_, Option>(4)?, created_at: row.get::<_, Option>(5)?, updated_at: row.get::<_, Option>(6)?, }) } ).map_err(|e| io::Error::new(io::ErrorKind::NotFound, e.to_string())) } fn query_children(&self, parent_uuid: &str) -> io::Result> { use rusqlite::Connection; let conn = Connection::open(&self.db_path) .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; let mut stmt = conn.prepare( "SELECT node_id, label, node_type, file_size, parent_id, created_at, updated_at FROM file_nodes WHERE parent_id = ? ORDER BY sort_order, label" ).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; let rows = stmt.query_map([parent_uuid], |row| { Ok(QueryNodeResult { node_id: row.get::<_, String>(0)?, label: row.get::<_, String>(1)?, node_type: row.get::<_, String>(2)?, file_size: row.get::<_, Option>(3)?, parent_id: row.get::<_, Option>(4)?, created_at: row.get::<_, Option>(5)?, updated_at: row.get::<_, Option>(6)?, }) }).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; let mut children = Vec::new(); for row in rows { children.push(row.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?); } Ok(children) } fn get_file_path(&self, uuid: &str) -> io::Result { use rusqlite::Connection; let conn = Connection::open(&self.db_path) .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; conn.query_row( "SELECT location FROM file_locations WHERE file_uuid = ?", [uuid], |row| row.get::<_, String>(0) ).map(PathBuf::from).map_err(|e| io::Error::new(io::ErrorKind::NotFound, e.to_string())) } } impl FileSystem for MarkBaseFs { type Inode = u64; type Handle = u64; fn init(&self, _capable: FsOptions) -> io::Result { println!("MarkBaseFs::init() called - filesystem ready"); println!("Database: {}", self.db_path.display()); println!("User: {}", self.user_id); println!("Backend: {}", self.backend.name()); Ok(FsOptions::empty()) } fn lookup(&self, _ctx: &Context, parent: Self::Inode, name: &CStr) -> io::Result { let parent_uuid = Self::ino_to_uuid(parent); let name_str = name.to_string_lossy(); let children = self.query_children(&parent_uuid)?; for child in children { if child.label == name_str { let child_ino = Self::uuid_to_ino(&child.node_id); let is_dir = child.node_type == "folder"; let mut stat: stat64 = unsafe { std::mem::zeroed() }; stat.st_ino = child_ino; stat.st_mode = if is_dir { 0o755 | libc::S_IFDIR } else { 0o644 | libc::S_IFREG }; stat.st_nlink = if is_dir { 2 } else { 1 }; stat.st_size = child.file_size.unwrap_or(0) as i64; stat.st_mtime = child.updated_at.unwrap_or(0); stat.st_ctime = child.created_at.unwrap_or(0); return Ok(Entry { inode: child_ino, generation: 0, attr: stat, attr_flags: 0, attr_timeout: Duration::from_secs(60), entry_timeout: Duration::from_secs(60), }); } } Err(io::Error::from_raw_os_error(libc::ENOENT)) } fn getattr( &self, _ctx: &Context, inode: Self::Inode, _handle: Option, ) -> io::Result<(stat64, Duration)> { let uuid = Self::ino_to_uuid(inode); let node = self.query_node(&uuid)?; let is_dir = node.node_type == "folder"; let mut stat: stat64 = unsafe { std::mem::zeroed() }; stat.st_ino = inode; stat.st_mode = if is_dir { 0o755 | libc::S_IFDIR } else { 0o644 | libc::S_IFREG }; stat.st_nlink = if is_dir { 2 } else { 1 }; stat.st_size = node.file_size.unwrap_or(0) as i64; stat.st_mtime = node.updated_at.unwrap_or(0); stat.st_ctime = node.created_at.unwrap_or(0); Ok((stat, Duration::from_secs(60))) } fn opendir( &self, _ctx: &Context, inode: Self::Inode, _flags: u32, ) -> io::Result<(Option, OpenOptions)> { Ok((Some(inode), OpenOptions::empty())) } fn readdir( &self, _ctx: &Context, inode: Self::Inode, _handle: Self::Handle, _size: u32, offset: u64, add_entry: &mut dyn FnMut(DirEntry) -> io::Result, ) -> io::Result<()> { let uuid = Self::ino_to_uuid(inode); let children = self.query_children(&uuid)?; for (idx, child) in children.iter().enumerate().skip(offset as usize) { let child_ino = Self::uuid_to_ino(&child.node_id); let type_ = if child.node_type == "folder" { DT_DIR } else { DT_REG }; let name_bytes = child.label.as_bytes(); let entry = DirEntry { ino: child_ino, offset: (idx + 1) as u64, type_: type_ as u32, name: name_bytes, }; match add_entry(entry) { Ok(0) => break, Ok(_) => continue, Err(e) => return Err(e), } } Ok(()) } fn releasedir( &self, _ctx: &Context, _inode: Self::Inode, _flags: u32, _handle: Self::Handle, ) -> io::Result<()> { Ok(()) } fn open( &self, _ctx: &Context, inode: Self::Inode, _flags: u32, _fuse_flags: u32, ) -> io::Result<(Option, OpenOptions, Option)> { Ok((Some(inode), OpenOptions::empty(), None)) } fn read( &self, _ctx: &Context, inode: Self::Inode, _handle: Self::Handle, w: &mut dyn fuse_backend_rs::api::filesystem::ZeroCopyWriter, size: u32, offset: u64, _lock_owner: Option, _flags: u32, ) -> io::Result { let uuid = Self::ino_to_uuid(inode); let path = self.get_file_path(&uuid)?; if !path.exists() { return Err(io::Error::from_raw_os_error(libc::ENOENT)); } let mut file = File::open(&path)?; file.seek(SeekFrom::Start(offset))?; let mut buffer = vec![0u8; size as usize]; let bytes_read = file.read(&mut buffer)?; w.write_all(&buffer[..bytes_read])?; Ok(bytes_read) } fn write( &self, _ctx: &Context, inode: Self::Inode, _handle: Self::Handle, r: &mut dyn fuse_backend_rs::api::filesystem::ZeroCopyReader, size: u32, offset: u64, _lock_owner: Option, _delayed_write: bool, _flags: u32, _fuse_flags: u32, ) -> io::Result { let uuid = Self::ino_to_uuid(inode); let path = self.get_file_path(&uuid)?; let mut file = File::create(&path)?; file.seek(SeekFrom::Start(offset))?; let mut buffer = vec![0u8; size as usize]; let bytes_read = r.read(&mut buffer)?; file.write_all(&buffer[..bytes_read])?; Ok(bytes_read) } fn release( &self, _ctx: &Context, _inode: Self::Inode, _flags: u32, _handle: Self::Handle, _flush: bool, _flock_release: bool, _lock_owner: Option, ) -> io::Result<()> { Ok(()) } fn statfs(&self, _ctx: &Context, _inode: Self::Inode) -> io::Result { let mut stat: statvfs64 = unsafe { std::mem::zeroed() }; stat.f_bsize = 4096; stat.f_frsize = 4096; stat.f_blocks = 1000000; stat.f_bfree = 500000; stat.f_bavail = 500000; stat.f_files = 12659; stat.f_ffree = 50000; stat.f_favail = 50000; Ok(stat) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_markbase_fs_creation() { let db_path = PathBuf::from("/tmp/test.sqlite"); let fs = MarkBaseFs::new("test_user".to_string(), db_path, BackendType::Fskit); assert_eq!(fs.get_user_id(), "test_user"); assert_eq!(fs.get_backend(), &BackendType::Fskit); } #[test] fn test_uuid_to_ino_conversion() { let uuid = "8b1ede3cd6970f02fa85b8e34b682caf"; let ino = MarkBaseFs::uuid_to_ino(uuid); let ino2 = MarkBaseFs::uuid_to_ino(uuid); assert_eq!(ino, ino2); assert!(ino > 0); } #[test] fn test_mount_placeholder() { let db_path = PathBuf::from("/tmp/test.sqlite"); let fs = MarkBaseFs::new("test_user".to_string(), db_path, BackendType::Nfs4); let mount_path = Path::new("/tmp/mount_test"); let result = fs.mount(mount_path); assert!(result.is_ok()); } }