aboutsummaryrefslogtreecommitdiff
path: root/src/client
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
downloadcarton-43e1a12b5bce11b4a28a53acca243e35c2be6d3e.tar.gz
Initial commit
Diffstat (limited to 'src/client')
-rw-r--r--src/client/fs.rs634
-rw-r--r--src/client/fs/dcache.rs107
-rw-r--r--src/client/fs/fcache.rs31
-rw-r--r--src/client/fs/fh.rs110
-rw-r--r--src/client/fs/file.rs78
-rw-r--r--src/client/fs/ino.rs51
6 files changed, 1011 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(())
+}
diff --git a/src/client/fs/dcache.rs b/src/client/fs/dcache.rs
new file mode 100644
index 0000000..b171abc
--- /dev/null
+++ b/src/client/fs/dcache.rs
@@ -0,0 +1,107 @@
+use super::ino::Ino;
+use std::{
+ collections::{
+ btree_map::{self, OccupiedError},
+ BTreeMap, HashMap,
+ },
+ ffi::{OsStr, OsString},
+};
+
+#[derive(Debug)]
+pub(super) struct Dcache {
+ inner: HashMap<Ino, Dentry>,
+}
+
+impl Dcache {
+ pub fn new() -> Self {
+ Self {
+ inner: HashMap::new(),
+ }
+ }
+
+ pub fn get_ino(&self, parent: Ino, name: &OsStr) -> Option<&Ino> {
+ self.get(parent).and_then(|dentry| dentry.get(name))
+ }
+
+ pub fn try_insert_name(
+ &mut self,
+ parent: Ino,
+ name: OsString,
+ ino: Ino,
+ ) -> Option<Result<(), ()>> {
+ match self
+ .get_mut(parent)
+ .map(|dentry| dentry.try_insert(name, ino))
+ {
+ Some(Ok(_)) => Some(Ok(())),
+ Some(Err(_)) => Some(Err(())),
+ None => None,
+ }
+ }
+
+ pub fn add_dentry(&mut self, ino: Ino, parent: Ino) -> Option<Dentry> {
+ self.insert(ino, Dentry::new(ino, parent))
+ }
+
+ // Map-like API
+
+ pub fn insert(&mut self, ino: Ino, dentry: Dentry) -> Option<Dentry> {
+ self.inner.insert(ino, dentry)
+ }
+
+ pub fn get(&self, ino: Ino) -> Option<&Dentry> {
+ self.inner.get(&ino)
+ }
+
+ pub fn get_mut(&mut self, ino: Ino) -> Option<&mut Dentry> {
+ self.inner.get_mut(&ino)
+ }
+
+ pub fn remove(&mut self, ino: Ino) -> Option<Dentry> {
+ self.inner.remove(&ino)
+ }
+}
+
+#[derive(Debug)]
+pub(super) struct Dentry {
+ inner: BTreeMap<OsString, Ino>,
+ pub loaded: bool,
+}
+
+impl Dentry {
+ pub fn new(ino: Ino, parent: Ino) -> Self {
+ let mut dentry = Self {
+ inner: BTreeMap::new(),
+ loaded: false,
+ };
+
+ dentry.insert(".".into(), ino);
+ dentry.insert("..".into(), parent);
+
+ dentry
+ }
+
+ pub fn insert(&mut self, k: OsString, v: Ino) -> Option<Ino> {
+ self.inner.insert(k, v)
+ }
+
+ pub fn try_insert(
+ &mut self,
+ k: OsString,
+ v: Ino,
+ ) -> Result<&mut Ino, btree_map::OccupiedError<'_, OsString, Ino>> {
+ self.inner.try_insert(k, v)
+ }
+
+ pub fn get(&self, k: &OsStr) -> Option<&Ino> {
+ self.inner.get(k)
+ }
+
+ pub fn remove(&mut self, k: &OsStr) -> Option<Ino> {
+ self.inner.remove(k)
+ }
+
+ pub fn iter(&self) -> impl Iterator<Item = (&OsString, &Ino)> {
+ self.inner.iter()
+ }
+}
diff --git a/src/client/fs/fcache.rs b/src/client/fs/fcache.rs
new file mode 100644
index 0000000..03785c0
--- /dev/null
+++ b/src/client/fs/fcache.rs
@@ -0,0 +1,31 @@
+use super::{file::File, ino::Ino};
+use std::collections::HashMap;
+
+#[derive(Debug)]
+pub struct Fcache {
+ inner: HashMap<Ino, File>,
+}
+
+impl Fcache {
+ pub fn new() -> Self {
+ Self {
+ inner: HashMap::new(),
+ }
+ }
+
+ pub fn insert(&mut self, ino: Ino, file: File) -> Option<File> {
+ self.inner.insert(ino, file)
+ }
+
+ pub fn get(&self, ino: Ino) -> Option<&File> {
+ self.inner.get(&ino)
+ }
+
+ pub fn get_mut(&mut self, ino: Ino) -> Option<&mut File> {
+ self.inner.get_mut(&ino)
+ }
+
+ pub fn remove(&mut self, ino: Ino) -> Option<File> {
+ self.inner.remove(&ino)
+ }
+}
diff --git a/src/client/fs/fh.rs b/src/client/fs/fh.rs
new file mode 100644
index 0000000..b1e8739
--- /dev/null
+++ b/src/client/fs/fh.rs
@@ -0,0 +1,110 @@
+use crate::{common::temp_file, server::Stream};
+use log::error;
+use std::{
+ fs::{self, File},
+ io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
+ ops::Index,
+ path::PathBuf,
+ slice::SliceIndex,
+};
+
+pub struct FileHandle {
+ buffer: Vec<u8>,
+ size: usize,
+ tmp: Option<(PathBuf, BufWriter<File>)>,
+ reader: Option<BufReader<File>>,
+ dirty: bool,
+}
+
+impl FileHandle {
+ pub fn empty() -> std::io::Result<Self> {
+ let (temp_path, temp_file) = temp_file()?;
+
+ Ok(Self {
+ buffer: Vec::new(),
+ size: 0,
+ tmp: Some((temp_path, BufWriter::new(temp_file))),
+ reader: None,
+ dirty: false,
+ })
+ }
+
+ pub fn new_rw(reader: BufReader<File>) -> std::io::Result<Self> {
+ // TODO: Copy read to tmp
+ let (temp_path, temp_file) = temp_file()?;
+
+ Ok(Self {
+ buffer: Vec::new(),
+ size: 0,
+ tmp: Some((temp_path, BufWriter::new(temp_file))),
+ reader: Some(reader),
+ dirty: false,
+ })
+ }
+
+ pub fn new_ro(reader: BufReader<File>) -> Self {
+ Self {
+ buffer: Vec::new(),
+ size: 0,
+ tmp: None,
+ reader: Some(reader),
+ dirty: false,
+ }
+ }
+
+ pub fn is_dirty(&self) -> bool {
+ self.dirty
+ }
+
+ pub fn write(&mut self, data: &[u8], offset: usize) -> Option<usize> {
+ let Some((_, ref mut tmp)) = self.tmp else {
+ error!("Tried to write to a RO file handle");
+ return None;
+ };
+
+ tmp.seek(SeekFrom::Start(offset as u64))
+ .inspect_err(|e| error!("Seek error: {e}"))
+ .ok()?;
+ let written = tmp
+ .write(data)
+ .inspect_err(|e| error!("Write error: {e}"))
+ .ok()?;
+
+ self.dirty = true;
+
+ Some(written)
+ }
+
+ pub fn read(&mut self, offset: usize, size: usize) -> Vec<u8> {
+ let Some(mut reader) = self.reader else {
+ error!("Tried to read from an empty file handle");
+ return Vec::new();
+ };
+
+ let mut buf = Vec::with_capacity(size);
+
+ // TODO: error handling...
+ reader.seek(SeekFrom::Start(offset as u64)).unwrap();
+ reader.read_exact(&mut buf).unwrap();
+
+ buf
+ }
+
+ pub fn clear(&mut self) {
+ self.tmp.as_mut().map(|b| b.1.flush());
+ self.buffer.clear();
+ self.dirty = false;
+ }
+}
+
+impl Drop for FileHandle {
+ fn drop(&mut self) {
+ let Some((ref temp_path, _)) = self.tmp else {
+ return;
+ };
+
+ if let Err(e) = fs::remove_file(temp_path) {
+ error!("Couldn't delete temp file {temp_path:?}: {e}");
+ }
+ }
+}
diff --git a/src/client/fs/file.rs b/src/client/fs/file.rs
new file mode 100644
index 0000000..5d51913
--- /dev/null
+++ b/src/client/fs/file.rs
@@ -0,0 +1,78 @@
+use super::ino::Ino;
+use crate::server::blobref::BlobRef;
+use fuser::{FileAttr, FileType};
+use std::{
+ ffi::{OsStr, OsString},
+ time::SystemTime,
+};
+
+const DEFAULT_PERMISSIONS: u16 = 0o644;
+
+#[derive(Debug)]
+pub struct File {
+ // Files only have a blobref if they were written to. No blob is created if the
+ // files is only `touch`ed. This means empty files will disappear on `umount`.
+ pub blobref: Option<BlobRef>,
+ parent: Option<Ino>,
+ pub attr: FileAttr,
+ name: OsString,
+}
+
+impl File {
+ fn new(ino: Ino, name: &OsStr, kind: FileType) -> Self {
+ let now = SystemTime::now();
+
+ let attr = FileAttr {
+ ino: ino.into(),
+ size: 0,
+ blocks: 0,
+ atime: now,
+ mtime: now,
+ ctime: now,
+ crtime: now,
+ kind,
+ perm: DEFAULT_PERMISSIONS,
+ nlink: 0,
+ uid: 0,
+ gid: 0,
+ rdev: 0,
+ flags: 0,
+ blksize: 0,
+ };
+
+ File {
+ blobref: None,
+ parent: None,
+ attr,
+ name: name.into(),
+ }
+ }
+
+ pub fn new_regular_file<T: Into<OsString>>(ino: Ino, name: T) -> Self {
+ Self::new(ino, &name.into(), FileType::RegularFile)
+ }
+
+ pub fn new_directory<T: Into<OsString>>(ino: Ino, name: T) -> Self {
+ Self::new(ino, &name.into(), FileType::Directory)
+ }
+
+ pub fn set_parent(&mut self, ino: Ino) {
+ self.parent = Some(ino);
+ }
+
+ pub fn name(&self) -> OsString {
+ self.name.clone()
+ }
+
+ pub fn parent(&self) -> Option<Ino> {
+ self.parent
+ }
+
+ pub fn ino(&self) -> Ino {
+ self.attr.ino.into()
+ }
+
+ pub fn size(&self) -> usize {
+ self.attr.size as usize
+ }
+}
diff --git a/src/client/fs/ino.rs b/src/client/fs/ino.rs
new file mode 100644
index 0000000..0b7628e
--- /dev/null
+++ b/src/client/fs/ino.rs
@@ -0,0 +1,51 @@
+use std::fmt::Display;
+
+const ROOT_INO: u64 = 1;
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash, Copy)]
+pub struct Ino(u64);
+
+impl Ino {
+ pub fn new() -> Self {
+ Self(ROOT_INO)
+ }
+
+ pub fn next(&mut self) -> &Self {
+ self.0 += 1;
+ self
+ }
+
+ pub fn prev(&mut self) {
+ self.0 -= 1;
+ }
+}
+
+impl From<Ino> for u64 {
+ fn from(ino: Ino) -> Self {
+ ino.0
+ }
+}
+
+impl From<u64> for Ino {
+ fn from(ino: u64) -> Self {
+ Self(ino)
+ }
+}
+
+impl From<u32> for Ino {
+ fn from(ino: u32) -> Self {
+ Self(u64::from(ino))
+ }
+}
+
+impl Display for Ino {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl Default for Ino {
+ fn default() -> Self {
+ Self::new()
+ }
+}