aboutsummaryrefslogtreecommitdiff
path: root/src/client/fs.rs
diff options
context:
space:
mode:
authorevuez <julien@mulga.net>2024-04-03 22:43:16 +0200
committerevuez <julien@mulga.net>2024-04-03 22:43:16 +0200
commit43e1a12b5bce11b4a28a53acca243e35c2be6d3e (patch)
tree07d64823718bfee063ab7b3d5721ac1e950ae17c /src/client/fs.rs
downloadcarton-43e1a12b5bce11b4a28a53acca243e35c2be6d3e.tar.gz
Initial commit
Diffstat (limited to 'src/client/fs.rs')
-rw-r--r--src/client/fs.rs634
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(())
+}