//! The mount on Linux: FUSE (`fuser`) over the overlay. use std::ffi::OsStr; use std::os::unix::fs::FileExt; use std::path::Path; use std::time::{Duration, SystemTime}; use anyhow::Result; use fuser::{ AccessFlags, Config, Errno, FileAttr, FileHandle, FileType, Filesystem, FopenFlags, Generation, INodeNo, LockOwner, MountOption, OpenFlags, RenameFlags, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite, Request, TimeOrNow, WriteFlags, }; use super::overlay::{self, Attr, Kind, Overlay, SetTime, State, cpath, join, parent, time}; use crate::app::App; /// `on mount --serve `: runs the mount until it's unmounted. pub fn serve(app: &App, root: &Path) -> Result<()> { let lazy = Lazy(Overlay::load(app, root)?); let mut config = Config::default(); config.mount_options = vec![MountOption::FSName("onset".into()), MountOption::Subtype("onset".into()), MountOption::DefaultPermissions, MountOption::NoAtime]; fuser::mount(lazy, root, &config)?; Ok(()) } const TTL_ENTRY: Duration = Duration::from_secs(1); /// Attributes are asked for every time: a save compares them with the index. const TTL_ATTR: Duration = Duration::ZERO; struct Lazy(Overlay); fn errno(e: overlay::Errno) -> Errno { Errno::from_i32(e.0) } fn set_time(t: Option) -> Option { match t { None => None, Some(TimeOrNow::Now) => Some(SetTime::Now), Some(TimeOrNow::SpecificTime(t)) => Some(SetTime::At(t)), } } fn file_attr(a: &Attr) -> FileAttr { FileAttr { ino: INodeNo(a.ino), size: a.size, blocks: a.blocks, atime: time(a.atime_ns), mtime: time(a.mtime_ns), ctime: time(a.ctime_ns), crtime: time(a.mtime_ns), kind: match a.kind { Kind::Dir => FileType::Directory, Kind::File => FileType::RegularFile, Kind::Link => FileType::Symlink, }, perm: a.perm, nlink: a.nlink, uid: a.uid, gid: a.gid, rdev: 0, blksize: 4096, flags: 0, } } impl Lazy { fn entry_reply(&self, st: &mut State, path: &str, reply: ReplyEntry) { match self.0.find(st, path) { Some(node) => { let a = file_attr(&self.0.attr(st, path, &node)); reply.entry(&TTL_ENTRY, &a, Generation(0)); } None => reply.error(Errno::ENOENT), } } fn attr_reply(&self, st: &mut State, ino: INodeNo, reply: ReplyAttr) { let Ok(path) = self.0.path_of(st, ino.0) else { return reply.error(Errno::ENOENT) }; match self.0.find(st, &path) { Some(node) => { let a = file_attr(&self.0.attr(st, &path, &node)); reply.attr(&TTL_ATTR, &a); } None => reply.error(Errno::ENOENT), } } } impl Filesystem for Lazy { fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) { let mut st = self.0.state.lock().unwrap(); match self.0.child(&st, parent.0, name) { Ok(path) => self.entry_reply(&mut st, &path, reply), Err(_) => reply.error(Errno::ENOENT), } } fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option, reply: ReplyAttr) { let mut st = self.0.state.lock().unwrap(); self.attr_reply(&mut st, ino, reply); } fn setattr( &self, _req: &Request, ino: INodeNo, 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: ReplyAttr, ) { let mut st = self.0.state.lock().unwrap(); let result = (|| -> Result<(), overlay::Errno> { let path = self.0.path_of(&st, ino.0)?; let open = fh.and_then(|h| st.handles.get(&h.0)); self.0.setattr(&st, &path, mode, size, open, set_time(atime), set_time(mtime)) })(); match result { Ok(()) => self.attr_reply(&mut st, ino, reply), Err(e) => reply.error(errno(e)), } } fn readlink(&self, _req: &Request, ino: INodeNo, reply: ReplyData) { let st = self.0.state.lock().unwrap(); let Ok(path) = self.0.path_of(&st, ino.0) else { return reply.error(Errno::ENOENT) }; match self.0.readlink(&st, &path) { Ok(target) => reply.data(&target), Err(e) => reply.error(errno(e)), } } fn mkdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, mode: u32, umask: u32, reply: ReplyEntry) { let mut st = self.0.state.lock().unwrap(); let result = (|| -> Result { let path = self.0.child(&st, parent.0, name)?; self.0.mkdir(&st, &path, mode & !umask)?; Ok(path) })(); match result { Ok(path) => self.entry_reply(&mut st, &path, reply), Err(e) => reply.error(errno(e)), } } fn unlink(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { let mut st = self.0.state.lock().unwrap(); let result = self.0.child(&st, parent.0, name).and_then(|path| self.0.unlink(&mut st, path)); match result { Ok(()) => reply.ok(), Err(e) => reply.error(errno(e)), } } fn rmdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { let mut st = self.0.state.lock().unwrap(); let result = self.0.child(&st, parent.0, name).and_then(|path| self.0.rmdir(&mut st, path)); match result { Ok(()) => reply.ok(), Err(e) => reply.error(errno(e)), } } fn symlink(&self, _req: &Request, parent: INodeNo, link_name: &OsStr, target: &Path, reply: ReplyEntry) { let mut st = self.0.state.lock().unwrap(); let result = (|| -> Result { let path = self.0.child(&st, parent.0, link_name)?; self.0.symlink(&st, &path, target)?; Ok(path) })(); match result { Ok(path) => self.entry_reply(&mut st, &path, reply), Err(e) => reply.error(errno(e)), } } fn rename(&self, _req: &Request, parent: INodeNo, name: &OsStr, newparent: INodeNo, newname: &OsStr, flags: RenameFlags, reply: ReplyEmpty) { let mut st = self.0.state.lock().unwrap(); let result = (|| -> Result<(), overlay::Errno> { if flags.contains(RenameFlags::RENAME_EXCHANGE) { return Err(overlay::Errno::EINVAL); } let from = self.0.child(&st, parent.0, name)?; let to = self.0.child(&st, newparent.0, newname)?; self.0.rename(&mut st, from, to, flags.contains(RenameFlags::RENAME_NOREPLACE)) })(); match result { Ok(()) => reply.ok(), Err(e) => reply.error(errno(e)), } } fn open(&self, _req: &Request, ino: INodeNo, flags: OpenFlags, reply: ReplyOpen) { let mut st = self.0.state.lock().unwrap(); let result = (|| -> Result { let path = self.0.path_of(&st, ino.0)?; let acc = flags.0 & libc::O_ACCMODE; let write = acc == libc::O_WRONLY || acc == libc::O_RDWR; let file = self.0.open(&st, &path, acc == libc::O_RDONLY || acc == libc::O_RDWR, write, flags.0 & libc::O_TRUNC != 0)?; let fh = st.next_fh; st.next_fh += 1; st.handles.insert(fh, file); Ok(fh) })(); match result { Ok(fh) => reply.opened(FileHandle(fh), FopenFlags::empty()), Err(e) => reply.error(errno(e)), } } fn read(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, offset: u64, size: u32, _flags: OpenFlags, _lock: Option, reply: ReplyData) { let st = self.0.state.lock().unwrap(); let Some(file) = st.handles.get(&fh.0) else { return reply.error(Errno::EBADF) }; let mut buf = vec![0u8; size as usize]; let mut got = 0; while got < buf.len() { match file.read_at(&mut buf[got..], offset + got as u64) { Ok(0) => break, Ok(n) => got += n, Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {} Err(e) => return reply.error(Errno::from(e)), } } reply.data(&buf[..got]); } fn write(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, offset: u64, data: &[u8], _wf: WriteFlags, _flags: OpenFlags, _lock: Option, reply: ReplyWrite) { let st = self.0.state.lock().unwrap(); let Some(file) = st.handles.get(&fh.0) else { return reply.error(Errno::EBADF) }; match file.write_all_at(data, offset) { Ok(()) => reply.written(data.len() as u32), Err(e) => reply.error(Errno::from(e)), } } fn flush(&self, _req: &Request, _ino: INodeNo, _fh: FileHandle, _lock: LockOwner, reply: ReplyEmpty) { reply.ok(); } fn release(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _flags: OpenFlags, _lock: Option, _flush: bool, reply: ReplyEmpty) { self.0.state.lock().unwrap().handles.remove(&fh.0); reply.ok(); } fn fsync(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _datasync: bool, reply: ReplyEmpty) { let st = self.0.state.lock().unwrap(); match st.handles.get(&fh.0).map(|f| f.sync_all()) { Some(Err(e)) => reply.error(Errno::from(e)), _ => reply.ok(), } } fn readdir(&self, _req: &Request, ino: INodeNo, _fh: FileHandle, offset: u64, mut reply: ReplyDirectory) { let mut st = self.0.state.lock().unwrap(); let Ok(path) = self.0.path_of(&st, ino.0) else { return reply.error(Errno::ENOENT) }; if !self.0.find(&st, &path).is_some_and(|n| n.is_dir()) { return reply.error(Errno::ENOTDIR); } let up = if path.is_empty() { 1 } else { self.0.ino_of(&mut st, parent(&path)) }; let mut list = vec![(ino.0, FileType::Directory, ".".to_string()), (up, FileType::Directory, "..".to_string())]; for (name, node) in self.0.listing(&st, &path) { let child = join(&path, &name); let kind = file_attr(&self.0.attr(&mut st, &child, &node)).kind; list.push((self.0.ino_of(&mut st, &child), kind, name)); } for (i, (child, kind, name)) in list.into_iter().enumerate().skip(offset as usize) { if reply.add(INodeNo(child), (i + 1) as u64, kind, name) { break; } } reply.ok(); } fn statfs(&self, _req: &Request, _ino: INodeNo, reply: ReplyStatfs) { let Ok(c) = cpath(&self.0.backing) else { return reply.error(Errno::EINVAL) }; let mut s: libc::statvfs = unsafe { std::mem::zeroed() }; // SAFETY: a NUL-terminated path and a statvfs to fill. if unsafe { libc::statvfs(c.as_ptr(), &mut s) } != 0 { return reply.error(Errno::from(std::io::Error::last_os_error())); } reply.statfs(s.f_blocks, s.f_bfree, s.f_bavail, s.f_files, s.f_ffree, s.f_bsize as u32, s.f_namemax as u32, s.f_frsize as u32); } fn access(&self, _req: &Request, _ino: INodeNo, _mask: AccessFlags, reply: ReplyEmpty) { reply.ok(); } fn create(&self, _req: &Request, parent: INodeNo, name: &OsStr, mode: u32, umask: u32, flags: i32, reply: ReplyCreate) { let mut st = self.0.state.lock().unwrap(); let result = (|| -> Result<(String, u64), overlay::Errno> { let path = self.0.child(&st, parent.0, name)?; let file = self.0.create(&st, &path, mode & !umask, flags & libc::O_EXCL != 0, flags & libc::O_TRUNC != 0)?; let fh = st.next_fh; st.next_fh += 1; st.handles.insert(fh, file); Ok((path, fh)) })(); match result { Ok((path, fh)) => match self.0.find(&st, &path) { Some(node) => { let a = file_attr(&self.0.attr(&mut st, &path, &node)); reply.created(&TTL_ENTRY, &a, Generation(0), FileHandle(fh), FopenFlags::empty()); } None => reply.error(Errno::EIO), }, Err(e) => reply.error(errno(e)), } } }