diff options
Diffstat (limited to 'src/client')
-rw-r--r-- | src/client/fs.rs | 634 | ||||
-rw-r--r-- | src/client/fs/dcache.rs | 107 | ||||
-rw-r--r-- | src/client/fs/fcache.rs | 31 | ||||
-rw-r--r-- | src/client/fs/fh.rs | 110 | ||||
-rw-r--r-- | src/client/fs/file.rs | 78 | ||||
-rw-r--r-- | src/client/fs/ino.rs | 51 |
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() + } +} |