mod dcache; mod fcache; mod fh; mod file; mod ino; use self::{dcache::Dcache, fcache::Fcache, fh::FileHandle, file::File, ino::Ino}; use crate::{ common::{ mime::{self, MimeType}, slot_map::SlotMap, }, server::{attrs::Attr, blobref::BlobRef, Server}, }; use fuser::TimeOrNow; use libc::{EBADF, EEXIST, EFBIG, EINVAL, EIO, ENFILE, ENOENT, O_RDONLY, O_RDWR, O_WRONLY}; use log::{debug, error, warn}; use std::{ cmp, ffi::{c_int, OsStr, OsString}, io, time::{Duration, SystemTime}, }; const GENERATION: u64 = 0; const CACHE_TTL: Duration = Duration::from_secs(1); pub struct FileSystem { server: Server, ino: Ino, dcache: Dcache, fcache: Fcache, // TODO: bcache ino -> blobref handles: SlotMap, } impl FileSystem { pub fn new(server: Server) -> Self { let ino = Ino::new(); let mut fs = FileSystem { server, ino, dcache: Dcache::new(), fcache: Fcache::new(), handles: SlotMap::new(), }; fs.setup_root(); fs } pub fn mount(self, mountpoint: &str) -> io::Result<()> { // TODO: Ignore permissions let opts = &[ fuser::MountOption::AllowOther, fuser::MountOption::AutoUnmount, fuser::MountOption::NoExec, ]; fuser::mount2(self, mountpoint, opts) } fn setup_root(&mut self) { let root_ino = self.ino; let parent_ino = self.next_ino(); self.dcache.add_dentry(root_ino, parent_ino); let mut root = File::new_directory(root_ino, "."); root.set_parent(parent_ino); self.fcache.insert(root_ino, root); self.fcache .insert(parent_ino, File::new_directory(parent_ino, "..")); } fn next_ino(&mut self) -> Ino { *self.ino.next() } fn empty_handle(&mut self) -> std::io::Result> { Ok(self.handles.insert(FileHandle::empty()?).map(u64::from)) } // fn release_handle(&mut self, fh: u64) { let fh = u32::try_from(fh).expect("Invalid file handle"); self.handles.remove(fh); } // fn file_by_name(&self, parent: Ino, name: &OsStr) -> Option<&File> { let ino = self.dcache.get_ino(parent, name)?; self.fcache.get(*ino) } // // fn get_blobref_by_name(&self, parent: u64, name: &OsStr) -> Option { // self.get_by_name(parent, name) // .and_then(|f| f.blobref.clone()) // } fn get_blobref_by_ino(&self, ino: Ino) -> Option { self.fcache.get(ino).and_then(|f| f.blobref.clone()) } fn get_parent_blobref(&self, file: &File) -> Option { file.parent().and_then(|ino| self.get_blobref_by_ino(ino)) } fn get_parent_blobref_by_ino(&self, ino: Ino) -> Option { self.fcache .get(ino) .and_then(|f| self.get_parent_blobref(f)) } // // fn remove_from_cache_by_name(&mut self, parent: u64, name: &OsStr) -> Option { // let ino = self // .dcache // .get_mut(&parent) // .and_then(|xs| xs.remove(name))?; // self.dcache.remove(&ino); // self.fcache.remove(&ino); // // Some(ino) // } } impl fuser::Filesystem for FileSystem { fn init( &mut self, _req: &fuser::Request<'_>, _config: &mut fuser::KernelConfig, ) -> Result<(), c_int> { Ok(()) } fn lookup( &mut self, _req: &fuser::Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEntry, ) { debug!("lookup(parent: {:#x?}, name {:?})", parent, name); if let Some(file) = self.file_by_name(parent.into(), name) { reply.entry(&CACHE_TTL, &file.attr, 0); } else { warn!("lookup(parent: {parent:#x?}, name {name:?}): ENOENT"); reply.error(ENOENT); } } fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { debug!("getattr(ino: {:#x?})", ino); if let Some(file) = self.fcache.get(ino.into()) { reply.attr(&CACHE_TTL, &file.attr); } else { warn!("getattr(ino: {:#x?}): ENOENT", ino); reply.error(ENOENT); } } fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { debug!("open(ino: {ino:#x?}, flags: {flags:b})"); // For now, we only support read-only, write-only and read-write. if flags & 0xf & !(O_RDONLY | O_WRONLY | O_RDWR) != 0 { error!("open(ino: {ino:#x?}): EIO: Unsupported mode"); reply.error(EIO); return; } // File should be cached first (via `readdir`). if self.fcache.get_mut(ino.into()).is_none() { error!("open(ino: {ino:#x?}): ENOENT"); reply.error(ENOENT); return; }; match self.empty_handle() { Ok(Some(fh)) => reply.opened(fh, u32::try_from(flags).expect("Invalid flags")), Ok(None) => { // No file handle available. error!("open(ino: {ino:#x?}): ENFILE"); reply.error(ENFILE); } Err(e) => { error!("open(ino: {ino:#x?}): EIO: {e}"); reply.error(EIO); } } } fn mkdir( &mut self, _req: &fuser::Request<'_>, parent: u64, name: &OsStr, mode: u32, umask: u32, reply: fuser::ReplyEntry, ) { debug!("mkdir(parent: {parent:#x?}, name: {name:?}, mode: {mode}, umask: {umask:#x?})"); let parent_blobref = self.get_blobref_by_ino(parent.into()); let Some(dentry) = self.dcache.get_mut(parent.into()) else { warn!("mkdir(parent: {parent:#x?}, name: {name:?}): ENOENT"); reply.error(ENOENT); return; }; self.ino.next(); if dentry.try_insert(name.into(), self.ino).is_err() { self.ino.prev(); warn!("mkdir(parent: {parent:#x?}, name: {name:?}): EEXIST"); reply.error(EEXIST); return; } let mut file = File::new_directory(self.ino, name); file.set_parent(parent.into()); let mut attrs = vec![Attr::Mime(MimeType::ApplicationXGroup)]; if let Some(b) = parent_blobref { attrs.push(Attr::Group(b)) }; if let Some(name) = name.to_str() { attrs.push(Attr::Name(name.to_string())); } else { warn!("mkdir(parent: {parent:#x?}, name: {name:?}): EINVAL"); reply.error(EINVAL); return; } let Ok(blobref) = self.server.put_with_attrs(&[] as &[u8], &attrs) else { dentry.remove(name); warn!("mkdir(parent: {parent:#x?}, name: {name:?}): EIO"); reply.error(EIO); return; }; file.blobref = Some(blobref); file.attr.mtime = SystemTime::now(); reply.entry(&CACHE_TTL, &file.attr, 0); self.dcache.add_dentry(self.ino, parent.into()); self.fcache.insert(self.ino, file); } fn readdir( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, offset: i64, mut reply: fuser::ReplyDirectory, ) { debug!("readdir(ino: {ino:#x?}, fh: {fh}, offset: {offset})"); let offset = usize::try_from(offset).expect("Invalid offset"); // Load dentry from the index // TODO: Move to opendir if let Err(e) = load_dentry( &self.server, &mut self.ino, ino.into(), &mut self.dcache, &mut self.fcache, ) { error!("readdir(ino: {ino:#x?}, fh: {fh}, offset: {offset}): {e:?}"); reply.error(e); return; } let Some(dentry) = self.dcache.get(ino.into()) else { warn!("readdir(ino: {ino:#x?}, fh: {fh}, offset: {offset}): ENOENT"); reply.error(ENOENT); return; }; for (i, (name, ino)) in dentry.iter().skip(offset).enumerate() { let Some(file) = self.fcache.get(*ino) else { error!("readdir(ino: {ino:#x?}, fh: {fh}, offset: {offset}): EIO"); reply.error(EIO); return; }; let curr_offset = i64::try_from(offset + i + 1).expect("Too many files in dentry"); let full = reply.add(file.attr.ino, curr_offset, file.attr.kind, name); if full { break; } } reply.ok(); } fn create( &mut self, _req: &fuser::Request<'_>, parent: u64, name: &std::ffi::OsStr, mode: u32, umask: u32, flags: i32, reply: fuser::ReplyCreate, ) { debug!( "create(parent: {:#x?}, name: {:?}, mode: {}, umask: {:#x?}, flags: {:#x?})", parent, name, mode, umask, flags ); let ino = self.next_ino(); match self.dcache.try_insert_name(parent.into(), name.into(), ino) { Some(Ok(())) => { let mut file = File::new_regular_file(ino, name); file.set_parent(parent.into()); file.attr.flags = u32::try_from(flags).unwrap_or(0); match self.empty_handle() { Ok(Some(fh)) => { reply.created(&CACHE_TTL, &file.attr, GENERATION, fh, file.attr.flags); self.fcache.insert(ino, file); } Ok(None) => { error!("create(ino: {ino:#x?}): ENFILE"); reply.error(ENFILE); } Err(e) => { error!("create(ino: {ino:#x?}): EIO: {e}"); reply.error(EIO); } } } Some(Err(())) => { warn!( "create(parent: {:#x?}, name: {:?}, mode: {}, umask: {:#x?}, flags: {:#x?}): EEXIST", parent, name, mode, umask, flags ); reply.error(EEXIST); } None => { warn!( "create(parent: {:#x?}, name: {:?}, mode: {}, umask: {:#x?}, flags: {:#x?}): ENOENT", parent, name, mode, umask, flags ); reply.error(ENOENT); } } } fn setattr( &mut self, _req: &fuser::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: fuser::ReplyAttr, ) { debug!( "setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \ gid: {:?}, size: {:?}, fh: {:?}, flags: {:?})", ino, mode, uid, gid, size, fh, flags ); if let Some(file) = self.fcache.get_mut(ino.into()) { if let Some(TimeOrNow::SpecificTime(t)) = atime { file.attr.atime = t; } if let Some(TimeOrNow::SpecificTime(t)) = mtime { file.attr.mtime = t; } file.attr.ctime = ctime.unwrap_or(file.attr.ctime); file.attr.size = size.unwrap_or(file.attr.size); file.attr.flags = flags.unwrap_or(file.attr.flags); reply.attr(&CACHE_TTL, &file.attr); } else { warn!("setattr(ino: {ino:#x?}): ENOENT"); reply.error(ENOENT); } } fn flush( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, lock_owner: u64, reply: fuser::ReplyEmpty, ) { debug!("flush(ino: {ino:#x?}, fh: {fh}, lock_owner: {lock_owner:?})"); let parent_blobref = self.get_parent_blobref_by_ino(ino.into()); let Some(file) = self.fcache.get_mut(ino.into()) else { warn!("flush(ino: {ino:#x?}): ENOENT"); reply.error(ENOENT); return; }; let fh = u32::try_from(fh).expect("Invalid file handle"); let Some(handle) = self.handles.get_mut(fh) else { warn!("flush(ino: {ino:#x?}): EBADF"); reply.error(EBADF); return; }; if !handle.is_dirty() { // Nothing to write reply.ok(); return; } file.attr.size = handle.buflen() as u64; file.attr.mtime = SystemTime::now(); let mut attrs = vec![Attr::CreatedAt(file.attr.crtime.into())]; if let Ok(name) = file.name().into_string() { if let Some(m) = mime::guess(&name) { attrs.push(Attr::Mime(m)) }; attrs.push(Attr::Name(name)); } if let Some(b) = parent_blobref { attrs.push(Attr::Group(b)) }; // TODO: self.server.append if file has a blobref already // -- or self.server.replace depending on whether we're // appending or replacing. let Ok(blobref) = self .server .put_with_attrs(handle.buffer().as_slice(), &attrs) else { // NOTE: Should we clear the handle on error too? // Unsure if we should be able to retry a flush? error!("flush(ino: {ino:#x?}): EIO"); reply.error(EIO); return; }; file.blobref = Some(blobref); handle.clear(); reply.ok(); } fn write( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, offset: i64, data: &[u8], write_flags: u32, flags: i32, lock_owner: Option, reply: fuser::ReplyWrite, ) { debug!( "write(ino: {ino:#x?}, fh: {fh}, offset: {offset}, size: {}, write_flags: {write_flags:#x?}, flags: {flags:#x?}, lock_owner: {lock_owner:?})", data.len(), ); let size: u32 = data.len().try_into().unwrap_or(0); if size < 1 && !data.is_empty() { // The block is too big. error!( "write(ino: {ino:#x?}, offset: {offset}, size: {}): EFBIG", data.len() ); reply.error(EFBIG); return; } let fh = u32::try_from(fh).expect("Invalid file handle"); // TODO: Should auto-flush when necessary if let Some(ref mut handle) = self.handles.get_mut(fh) { let offset = usize::try_from(offset).expect("Invalid offset"); // FIXME: Get written size from handle.write result if handle.write(data, offset).is_none() { error!( "write(ino: {ino:#x?}, offset: {offset}, size: {}): EIO", data.len() ); reply.error(EIO); return; } reply.written(size); } else { warn!("write(ino: {ino:#x?}): EBADF"); reply.error(EBADF); } } fn read( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, offset: i64, size: u32, flags: i32, lock_owner: Option, reply: fuser::ReplyData, ) { debug!( "read(ino: {:#x?}, fh: {}, offset: {}, size: {}, \ flags: {:#x?}, lock_owner: {:?})", ino, fh, offset, size, flags, lock_owner ); let Some(file) = self.fcache.get(ino.into()) else { warn!("read(ino: {ino:#x?}): EBADF"); reply.error(ENOENT); return; }; let fh = u32::try_from(fh).expect("Invalid file handle"); let Some(handle) = self.handles.get_mut(fh) else { warn!("read(ino: {ino:#x?}): EBADF"); reply.error(EBADF); return; }; // TODO: Check if offset > handle.buflen() or file.size() let offset = usize::try_from(offset).expect("Invalid offset"); let Some(ref blobref) = file.blobref else { // We haven't flushed the handle yet, but we should still be able to read from it reply.data(&handle.read(offset, size as usize)); return; }; let Ok(bytes) = self.server.read(blobref, offset..(offset + size as usize)) else { warn!("read(ino: {ino:#x?}): EIO"); reply.error(EIO); return; }; reply.data(&bytes); } fn release( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, _flags: i32, _lock_owner: Option, _flush: bool, // TODO: flush if true reply: fuser::ReplyEmpty, ) { debug!("release(ino: {ino:#x?}, fh: {fh})"); self.release_handle(fh); reply.ok(); } } fn load_dentry( server: &Server, ino: &mut Ino, parent_ino: Ino, dcache: &mut Dcache, fcache: &mut Fcache, ) -> Result<(), c_int> { let dentry = dcache.get_mut(parent_ino).ok_or(ENOENT)?; if dentry.loaded { return Ok(()); } let parent_blobref = fcache .get(parent_ino) .and_then(|f| f.blobref.clone()) .map(Attr::Group); // TODO: Pass range to `load_dentry` let objects = server.list(0..10_000, parent_blobref).map_err(|_| EIO)?; let mut dir_inos = Vec::new(); for object in objects { let ino = ino.next(); let name: OsString = object .get_name() .unwrap_or(format!(".carton.x-nameless-{ino}")) .into(); let file = if object.is_group() { let mut file = File::new_directory(*ino, name.clone()); file.blobref = Some(object.blobref().clone()); dir_inos.push(*ino); file } else { let mut file = File::new_regular_file(*ino, name.clone()); file.blobref = Some(object.blobref().clone()); file.attr.size = object.size() as u64; file }; dentry.insert(name, *ino); fcache.insert(*ino, file); } dentry.loaded = true; for dir_ino in &dir_inos { dcache.add_dentry(*dir_ino, parent_ino); } Ok(()) }