Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 5 | use std::path::Path; |
| 6 | use std::time::{Duration, SystemTime}; |
| 7 |
| 8 | use anyhow::Result; |
| 9 | use fuser::{ |
| 10 | AccessFlags, Config, Errno, FileAttr, FileHandle, FileType, Filesystem, FopenFlags, Generation, INodeNo, LockOwner, MountOption, OpenFlags, RenameFlags, ReplyAttr, ReplyCreate, |
| 11 | ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite, Request, TimeOrNow, WriteFlags, |
| 12 | }; |
| 13 |
| 14 | use super::overlay::{self, Attr, Kind, Overlay, SetTime, State, cpath, join, parent, time}; |
| 15 | use crate::app::App; |
| 16 |
| 17 | /// `on mount --serve <folder>`: runs the mount until it's unmounted. |
| 18 | pub fn serve(app: &App, root: &Path) -> Result<()> { |
| 19 | let lazy = Lazy(Overlay::load(app, root)?); |
| 20 | let mut config = Config::default(); |
| 21 | config.mount_options = vec![MountOption::FSName("onset".into()), MountOption::Subtype("onset".into()), MountOption::DefaultPermissions, MountOption::NoAtime]; |
| 22 | fuser::mount(lazy, root, &config)?; |
| 23 | Ok(()) |
| 24 | } |
| 25 |
| 26 | const TTL_ENTRY: Duration = Duration::from_secs(1); |
| 27 | /// Attributes are asked for every time: a save compares them with the index. |
| 28 | const TTL_ATTR: Duration = Duration::ZERO; |
| 29 |
| 30 | struct Lazy(Overlay); |
| 31 |
| 32 | fn errno(e: overlay::Errno) -> Errno { |
| 33 | Errno::from_i32(e.0) |
| 34 | } |
| 35 |
| 36 | fn set_time(t: Option<TimeOrNow>) -> Option<SetTime> { |
| 37 | match t { |
| 38 | None => None, |
| 39 | Some(TimeOrNow::Now) => Some(SetTime::Now), |
| 40 | Some(TimeOrNow::SpecificTime(t)) => Some(SetTime::At(t)), |
| 41 | } |
| 42 | } |
| 43 |
| 44 | fn file_attr(a: &Attr) -> FileAttr { |
| 45 | FileAttr { |
| 46 | ino: INodeNo(a.ino), |
| 47 | size: a.size, |
| 48 | blocks: a.blocks, |
| 49 | atime: time(a.atime_ns), |
| 50 | mtime: time(a.mtime_ns), |
| 51 | ctime: time(a.ctime_ns), |
| 52 | crtime: time(a.mtime_ns), |
| 53 | kind: match a.kind { |
| 54 | Kind::Dir => FileType::Directory, |
| 55 | Kind::File => FileType::RegularFile, |
| 56 | Kind::Link => FileType::Symlink, |
| 57 | }, |
| 58 | perm: a.perm, |
| 59 | nlink: a.nlink, |
| 60 | uid: a.uid, |
| 61 | gid: a.gid, |
| 62 | rdev: 0, |
| 63 | blksize: 4096, |
| 64 | flags: 0, |
| 65 | } |
| 66 | } |
| 67 |
| 68 | impl Lazy { |
| 69 | fn entry_reply(&self, st: &mut State, path: &str, reply: ReplyEntry) { |
| 70 | match self.0.find(st, path) { |
| 71 | Some(node) => { |
| 72 | let a = file_attr(&self.0.attr(st, path, &node)); |
| 73 | reply.entry(&TTL_ENTRY, &a, Generation(0)); |
| 74 | } |
| 75 | None => reply.error(Errno::ENOENT), |
| 76 | } |
| 77 | } |
| 78 |
| 79 | fn attr_reply(&self, st: &mut State, ino: INodeNo, reply: ReplyAttr) { |
| 80 | let Ok(path) = self.0.path_of(st, ino.0) else { return reply.error(Errno::ENOENT) }; |
| 81 | match self.0.find(st, &path) { |
| 82 | Some(node) => { |
| 83 | let a = file_attr(&self.0.attr(st, &path, &node)); |
| 84 | reply.attr(&TTL_ATTR, &a); |
| 85 | } |
| 86 | None => reply.error(Errno::ENOENT), |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 |
| 91 | impl Filesystem for Lazy { |
| 92 | fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) { |
| 93 | let mut st = self.0.state.lock().unwrap(); |
| 94 | match self.0.child(&st, parent.0, name) { |
| 95 | Ok(path) => self.entry_reply(&mut st, &path, reply), |
| 96 | Err(_) => reply.error(Errno::ENOENT), |
| 97 | } |
| 98 | } |
| 99 |
| 100 | fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option<FileHandle>, reply: ReplyAttr) { |
| 101 | let mut st = self.0.state.lock().unwrap(); |
| 102 | self.attr_reply(&mut st, ino, reply); |
| 103 | } |
| 104 |
| 105 | fn setattr( |
| 106 | &self, |
| 107 | _req: &Request, |
| 108 | ino: INodeNo, |
| 109 | mode: Option<u32>, |
| 110 | _uid: Option<u32>, |
| 111 | _gid: Option<u32>, |
| 112 | size: Option<u64>, |
| 113 | atime: Option<TimeOrNow>, |
| 114 | mtime: Option<TimeOrNow>, |
| 115 | _ctime: Option<SystemTime>, |
| 116 | fh: Option<FileHandle>, |
| 117 | _crtime: Option<SystemTime>, |
| 118 | _chgtime: Option<SystemTime>, |
| 119 | _bkuptime: Option<SystemTime>, |
| 120 | _flags: Option<fuser::BsdFileFlags>, |
| 121 | reply: ReplyAttr, |
| 122 | ) { |
| 123 | let mut st = self.0.state.lock().unwrap(); |
| 124 | let result = (|| -> Result<(), overlay::Errno> { |
| 125 | let path = self.0.path_of(&st, ino.0)?; |
| 126 | let open = fh.and_then(|h| st.handles.get(&h.0)); |
| 127 | self.0.setattr(&st, &path, mode, size, open, set_time(atime), set_time(mtime)) |
| 128 | })(); |
| 129 | match result { |
| 130 | Ok(()) => self.attr_reply(&mut st, ino, reply), |
| 131 | Err(e) => reply.error(errno(e)), |
| 132 | } |
| 133 | } |
| 134 |
| 135 | fn readlink(&self, _req: &Request, ino: INodeNo, reply: ReplyData) { |
| 136 | let st = self.0.state.lock().unwrap(); |
| 137 | let Ok(path) = self.0.path_of(&st, ino.0) else { return reply.error(Errno::ENOENT) }; |
| 138 | match self.0.readlink(&st, &path) { |
| 139 | Ok(target) => reply.data(&target), |
| 140 | Err(e) => reply.error(errno(e)), |
| 141 | } |
| 142 | } |
| 143 |
| 144 | fn mkdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, mode: u32, umask: u32, reply: ReplyEntry) { |
| 145 | let mut st = self.0.state.lock().unwrap(); |
| 146 | let result = (|| -> Result<String, overlay::Errno> { |
| 147 | let path = self.0.child(&st, parent.0, name)?; |
| 148 | self.0.mkdir(&st, &path, mode & !umask)?; |
| 149 | Ok(path) |
| 150 | })(); |
| 151 | match result { |
| 152 | Ok(path) => self.entry_reply(&mut st, &path, reply), |
| 153 | Err(e) => reply.error(errno(e)), |
| 154 | } |
| 155 | } |
| 156 |
| 157 | fn unlink(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { |
| 158 | let mut st = self.0.state.lock().unwrap(); |
| 159 | let result = self.0.child(&st, parent.0, name).and_then(|path| self.0.unlink(&mut st, path)); |
| 160 | match result { |
| 161 | Ok(()) => reply.ok(), |
| 162 | Err(e) => reply.error(errno(e)), |
| 163 | } |
| 164 | } |
| 165 |
| 166 | fn rmdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { |
| 167 | let mut st = self.0.state.lock().unwrap(); |
| 168 | let result = self.0.child(&st, parent.0, name).and_then(|path| self.0.rmdir(&mut st, path)); |
| 169 | match result { |
| 170 | Ok(()) => reply.ok(), |
| 171 | Err(e) => reply.error(errno(e)), |
| 172 | } |
| 173 | } |
| 174 |
| 175 | fn symlink(&self, _req: &Request, parent: INodeNo, link_name: &OsStr, target: &Path, reply: ReplyEntry) { |
| 176 | let mut st = self.0.state.lock().unwrap(); |
| 177 | let result = (|| -> Result<String, overlay::Errno> { |
| 178 | let path = self.0.child(&st, parent.0, link_name)?; |
| 179 | self.0.symlink(&st, &path, target)?; |
| 180 | Ok(path) |
| 181 | })(); |
| 182 | match result { |
| 183 | Ok(path) => self.entry_reply(&mut st, &path, reply), |
| 184 | Err(e) => reply.error(errno(e)), |
| 185 | } |
| 186 | } |
| 187 |
| 188 | fn rename(&self, _req: &Request, parent: INodeNo, name: &OsStr, newparent: INodeNo, newname: &OsStr, flags: RenameFlags, reply: ReplyEmpty) { |
| 189 | let mut st = self.0.state.lock().unwrap(); |
| 190 | let result = (|| -> Result<(), overlay::Errno> { |
| 191 | if flags.contains(RenameFlags::RENAME_EXCHANGE) { |
| 192 | return Err(overlay::Errno::EINVAL); |
| 193 | } |
| 194 | let from = self.0.child(&st, parent.0, name)?; |
| 195 | let to = self.0.child(&st, newparent.0, newname)?; |
| 196 | self.0.rename(&mut st, from, to, flags.contains(RenameFlags::RENAME_NOREPLACE)) |
| 197 | })(); |
| 198 | match result { |
| 199 | Ok(()) => reply.ok(), |
| 200 | Err(e) => reply.error(errno(e)), |
| 201 | } |
| 202 | } |
| 203 |
| 204 | fn open(&self, _req: &Request, ino: INodeNo, flags: OpenFlags, reply: ReplyOpen) { |
| 205 | let mut st = self.0.state.lock().unwrap(); |
| 206 | let result = (|| -> Result<u64, overlay::Errno> { |
| 207 | let path = self.0.path_of(&st, ino.0)?; |
| 208 | let acc = flags.0 & libc::O_ACCMODE; |
| 209 | let write = acc == libc::O_WRONLY || acc == libc::O_RDWR; |
| 210 | let file = self.0.open(&st, &path, acc == libc::O_RDONLY || acc == libc::O_RDWR, write, flags.0 & libc::O_TRUNC != 0)?; |
| 211 | let fh = st.next_fh; |
| 212 | st.next_fh += 1; |
| 213 | st.handles.insert(fh, file); |
| 214 | Ok(fh) |
| 215 | })(); |
| 216 | match result { |
| 217 | Ok(fh) => reply.opened(FileHandle(fh), FopenFlags::empty()), |
| 218 | Err(e) => reply.error(errno(e)), |
| 219 | } |
| 220 | } |
| 221 |
| 222 | fn read(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, offset: u64, size: u32, _flags: OpenFlags, _lock: Option<LockOwner>, reply: ReplyData) { |
| 223 | let st = self.0.state.lock().unwrap(); |
| 224 | let Some(file) = st.handles.get(&fh.0) else { return reply.error(Errno::EBADF) }; |
| 225 | let mut buf = vec![0u8; size as usize]; |
| 226 | let mut got = 0; |
| 227 | while got < buf.len() { |
| 228 | match file.read_at(&mut buf[got..], offset + got as u64) { |
| 229 | Ok(0) => break, |
| 230 | Ok(n) => got += n, |
| 231 | Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {} |
| 232 | Err(e) => return reply.error(Errno::from(e)), |
| 233 | } |
| 234 | } |
| 235 | reply.data(&buf[..got]); |
| 236 | } |
| 237 |
| 238 | fn write(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, offset: u64, data: &[u8], _wf: WriteFlags, _flags: OpenFlags, _lock: Option<LockOwner>, reply: ReplyWrite) { |
| 239 | let st = self.0.state.lock().unwrap(); |
| 240 | let Some(file) = st.handles.get(&fh.0) else { return reply.error(Errno::EBADF) }; |
| 241 | match file.write_all_at(data, offset) { |
| 242 | Ok(()) => reply.written(data.len() as u32), |
| 243 | Err(e) => reply.error(Errno::from(e)), |
| 244 | } |
| 245 | } |
| 246 |
| 247 | fn flush(&self, _req: &Request, _ino: INodeNo, _fh: FileHandle, _lock: LockOwner, reply: ReplyEmpty) { |
| 248 | reply.ok(); |
| 249 | } |
| 250 |
| 251 | fn release(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _flags: OpenFlags, _lock: Option<LockOwner>, _flush: bool, reply: ReplyEmpty) { |
| 252 | self.0.state.lock().unwrap().handles.remove(&fh.0); |
| 253 | reply.ok(); |
| 254 | } |
| 255 |
| 256 | fn fsync(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _datasync: bool, reply: ReplyEmpty) { |
| 257 | let st = self.0.state.lock().unwrap(); |
| 258 | match st.handles.get(&fh.0).map(|f| f.sync_all()) { |
| 259 | Some(Err(e)) => reply.error(Errno::from(e)), |
| 260 | _ => reply.ok(), |
| 261 | } |
| 262 | } |
| 263 |
| 264 | fn readdir(&self, _req: &Request, ino: INodeNo, _fh: FileHandle, offset: u64, mut reply: ReplyDirectory) { |
| 265 | let mut st = self.0.state.lock().unwrap(); |
| 266 | let Ok(path) = self.0.path_of(&st, ino.0) else { return reply.error(Errno::ENOENT) }; |
| 267 | if !self.0.find(&st, &path).is_some_and(|n| n.is_dir()) { |
| 268 | return reply.error(Errno::ENOTDIR); |
| 269 | } |
| 270 | let up = if path.is_empty() { 1 } else { self.0.ino_of(&mut st, parent(&path)) }; |
| 271 | let mut list = vec![(ino.0, FileType::Directory, ".".to_string()), (up, FileType::Directory, "..".to_string())]; |
| 272 | for (name, node) in self.0.listing(&st, &path) { |
| 273 | let child = join(&path, &name); |
| 274 | let kind = file_attr(&self.0.attr(&mut st, &child, &node)).kind; |
| 275 | list.push((self.0.ino_of(&mut st, &child), kind, name)); |
| 276 | } |
| 277 | for (i, (child, kind, name)) in list.into_iter().enumerate().skip(offset as usize) { |
| 278 | if reply.add(INodeNo(child), (i + 1) as u64, kind, name) { |
| 279 | break; |
| 280 | } |
| 281 | } |
| 282 | reply.ok(); |
| 283 | } |
| 284 |
| 285 | fn statfs(&self, _req: &Request, _ino: INodeNo, reply: ReplyStatfs) { |
| 286 | let Ok(c) = cpath(&self.0.backing) else { return reply.error(Errno::EINVAL) }; |
| 287 | let mut s: libc::statvfs = unsafe { std::mem::zeroed() }; |
| 288 | // SAFETY: a NUL-terminated path and a statvfs to fill. |
| 289 | if unsafe { libc::statvfs(c.as_ptr(), &mut s) } != 0 { |
| 290 | return reply.error(Errno::from(std::io::Error::last_os_error())); |
| 291 | } |
| 292 | 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); |
| 293 | } |
| 294 |
| 295 | fn access(&self, _req: &Request, _ino: INodeNo, _mask: AccessFlags, reply: ReplyEmpty) { |
| 296 | reply.ok(); |
| 297 | } |
| 298 |
| 299 | fn create(&self, _req: &Request, parent: INodeNo, name: &OsStr, mode: u32, umask: u32, flags: i32, reply: ReplyCreate) { |
| 300 | let mut st = self.0.state.lock().unwrap(); |
| 301 | let result = (|| -> Result<(String, u64), overlay::Errno> { |
| 302 | let path = self.0.child(&st, parent.0, name)?; |
| 303 | let file = self.0.create(&st, &path, mode & !umask, flags & libc::O_EXCL != 0, flags & libc::O_TRUNC != 0)?; |
| 304 | let fh = st.next_fh; |
| 305 | st.next_fh += 1; |
| 306 | st.handles.insert(fh, file); |
| 307 | Ok((path, fh)) |
| 308 | })(); |
| 309 | match result { |
| 310 | Ok((path, fh)) => match self.0.find(&st, &path) { |
| 311 | Some(node) => { |
| 312 | let a = file_attr(&self.0.attr(&mut st, &path, &node)); |
| 313 | reply.created(&TTL_ENTRY, &a, Generation(0), FileHandle(fh), FopenFlags::empty()); |
| 314 | } |
| 315 | None => reply.error(Errno::EIO), |
| 316 | }, |
| 317 | Err(e) => reply.error(errno(e)), |
| 318 | } |
| 319 | } |
| 320 | } |
| 321 |