diff options
author | evuez <julien@mulga.net> | 2024-04-03 22:43:16 +0200 |
---|---|---|
committer | evuez <julien@mulga.net> | 2024-04-03 22:43:16 +0200 |
commit | 43e1a12b5bce11b4a28a53acca243e35c2be6d3e (patch) | |
tree | 07d64823718bfee063ab7b3d5721ac1e950ae17c /src/client/fs.rs | |
download | carton-43e1a12b5bce11b4a28a53acca243e35c2be6d3e.tar.gz |
Initial commit
Diffstat (limited to 'src/client/fs.rs')
-rw-r--r-- | src/client/fs.rs | 634 |
1 files changed, 634 insertions, 0 deletions
diff --git a/src/client/fs.rs b/src/client/fs.rs new file mode 100644 index 0000000..10949f0 --- /dev/null +++ b/src/client/fs.rs @@ -0,0 +1,634 @@ +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<FileHandle>, +} + +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<Option<u64>> { + 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<BlobRef> { + // self.get_by_name(parent, name) + // .and_then(|f| f.blobref.clone()) + // } + + fn get_blobref_by_ino(&self, ino: Ino) -> Option<BlobRef> { + self.fcache.get(ino).and_then(|f| f.blobref.clone()) + } + + fn get_parent_blobref(&self, file: &File) -> Option<BlobRef> { + file.parent().and_then(|ino| self.get_blobref_by_ino(ino)) + } + + fn get_parent_blobref_by_ino(&self, ino: Ino) -> Option<BlobRef> { + 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<u64> { + // 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<u32>, + uid: Option<u32>, + gid: Option<u32>, + size: Option<u64>, + atime: Option<fuser::TimeOrNow>, + mtime: Option<fuser::TimeOrNow>, + ctime: Option<std::time::SystemTime>, + fh: Option<u64>, + _crtime: Option<std::time::SystemTime>, + _chgtime: Option<std::time::SystemTime>, + _bkuptime: Option<std::time::SystemTime>, + flags: Option<u32>, + 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<u64>, + 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<u64>, + 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<u64>, + _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(()) +} |