From b51c680af4629bdae2fcca7ebb8edca0fa6b3c39 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 24 Jun 2026 21:56:12 +0200 Subject: [PATCH] Handle updates of source files --- .envrc | 1 + Cargo.lock | 232 ++++++++++++++++++++++++++++++++++------- Cargo.toml | 2 +- devenv.nix | 1 + justfile | 2 + src/file_watcher.rs | 3 + src/json_filesystem.rs | 175 ------------------------------- src/local.rs | 110 ++++++++++++++++--- src/main.rs | 11 +- 9 files changed, 310 insertions(+), 227 deletions(-) create mode 100644 justfile create mode 100644 src/file_watcher.rs delete mode 100644 src/json_filesystem.rs diff --git a/.envrc b/.envrc index d9f8be7..3036deb 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,5 @@ #!/usr/bin/env bash +export GIT_CONFIG_GLOBAL=/dev/null eval "$(devenv direnvrc)" diff --git a/Cargo.lock b/Cargo.lock index e37e0c9..bf44a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +49,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -139,7 +139,7 @@ checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", "nix 0.31.3", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -166,6 +166,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fuser" version = "0.17.0" @@ -208,6 +217,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -215,10 +244,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "itoa" -version = "1.0.18" +name = "kqueue" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags", + "libc", +] [[package]] name = "libc" @@ -256,6 +299,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "musicfs" version = "0.1.0" @@ -264,7 +319,7 @@ dependencies = [ "ctrlc", "fuser", "libc", - "serde_json", + "notify", "time", ] @@ -293,6 +348,33 @@ dependencies = [ "libc", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -449,21 +531,21 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -484,19 +566,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - [[package]] name = "smallvec" version = "1.15.2" @@ -581,6 +650,22 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "winapi" version = "0.3.9" @@ -597,6 +682,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -609,6 +703,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -618,6 +721,71 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "1.0.3" @@ -646,9 +814,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index cbee114..939ab9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,5 @@ clap = { version = "4.6.1", features = ["derive"] } ctrlc = "3.5.2" fuser = "0.17.0" libc = "0.2.186" -serde_json = "1.0.150" +notify = "8.2.0" time = "0.3.49" diff --git a/devenv.nix b/devenv.nix index 92eded6..f836c61 100644 --- a/devenv.nix +++ b/devenv.nix @@ -27,6 +27,7 @@ packages = with pkgs; [ git just + opencode ]; outputs = { diff --git a/justfile b/justfile new file mode 100644 index 0000000..18a4bc0 --- /dev/null +++ b/justfile @@ -0,0 +1,2 @@ +build: + cargo build diff --git a/src/file_watcher.rs b/src/file_watcher.rs new file mode 100644 index 0000000..2c1ddff --- /dev/null +++ b/src/file_watcher.rs @@ -0,0 +1,3 @@ +pub trait FileWatcher { + fn watch(&self); +} diff --git a/src/json_filesystem.rs b/src/json_filesystem.rs deleted file mode 100644 index 1ff25fe..0000000 --- a/src/json_filesystem.rs +++ /dev/null @@ -1,175 +0,0 @@ -use fuser::{ - Errno, FileAttr, FileType, Filesystem, Generation, INodeNo, ReplyAttr, ReplyDirectory, Request, -}; -use serde_json::{Map, Value}; -use std::collections::BTreeMap; -use std::path::Path; -use std::time::{Duration, SystemTime}; - -pub struct JsonFilesystem { - tree: Map, - attrs: BTreeMap, - inodes: BTreeMap, -} - -impl JsonFilesystem { - pub fn new(tree: &Map) -> JsonFilesystem { - let mut attrs = BTreeMap::new(); - let mut inodes = BTreeMap::new(); - let ts = SystemTime::now(); - let attr = FileAttr { - ino: INodeNo::ROOT, - size: 0, - blocks: 0, - atime: ts, - mtime: ts, - ctime: ts, - crtime: ts, - kind: FileType::Directory, - perm: 0o755, - nlink: 0, - uid: 0, - gid: 0, - rdev: 0, - blksize: 0, - flags: 0, - }; - - attrs.insert(INodeNo(1), attr); - inodes.insert("/".to_string(), INodeNo(1)); - - for (i, (key, value)) in tree.iter().enumerate() { - let attr = FileAttr { - ino: INodeNo(i as u64 + 2), - size: value.to_string().len() as u64, - blocks: 0, - atime: ts, - mtime: ts, - ctime: ts, - crtime: ts, - kind: FileType::RegularFile, - perm: 0o644, - nlink: 0, - uid: 0, - gid: 0, - rdev: 0, - blksize: 0, - flags: 0, - }; - - attrs.insert(attr.ino, attr); - inodes.insert(key.clone(), attr.ino); - } - - return JsonFilesystem { - tree: tree.clone(), - attrs: attrs, - inodes: inodes, - }; - } -} - -impl Filesystem for JsonFilesystem { - // For stat function like `stat /path/to/fuse` - fn getattr( - &self, - _req: &Request, - ino: fuser::INodeNo, - fh: Option, - reply: ReplyAttr, - ) { - println!("getattr(ino={})", ino); - - match self.attrs.get(&ino) { - Some(attr) => { - let ttl = Duration::new(1, 0); - reply.attr(&ttl, attr); - } - None => reply.error(Errno::ENOENT), - }; - } - - fn readdir( - &self, - _req: &Request, - ino: INodeNo, - fh: fuser::FileHandle, - offset: u64, - mut reply: ReplyDirectory, - ) { - println!("readdir(ino={}, fh={}, offset={})", ino, fh, offset); - - if ino == INodeNo::ROOT { - if offset == 0 { - let _ = reply.add(INodeNo::ROOT, 0, FileType::Directory, &Path::new(".")); - let _ = reply.add(INodeNo::ROOT, 1, FileType::Directory, &Path::new("..")); - for (i, key) in self.tree.keys().enumerate() { - let inode: u64 = 2 + i as u64; - let offset: u64 = 2 + i as u64; - let _ = reply.add( - INodeNo(inode), - offset, - FileType::RegularFile, - &Path::new(key), - ); - } - } - reply.ok(); - } else { - reply.error(Errno::ENOSYS); - } - } - - fn lookup( - &self, - _req: &Request, - parent: INodeNo, - name: &std::ffi::OsStr, - reply: fuser::ReplyEntry, - ) { - println!("lookup(parent={}, name={})", parent, name.display()); - - let inode = match self.inodes.get(name.to_str().unwrap()) { - Some(inode) => inode, - None => { - reply.error(Errno::ENOENT); - return; - } - }; - - match self.attrs.get(inode) { - Some(attr) => { - let ttl = Duration::new(1, 0); - reply.entry(&ttl, attr, Generation(0)); - } - None => reply.error(Errno::ENOENT), - }; - } - - fn read( - &self, - _req: &Request, - ino: INodeNo, - fh: fuser::FileHandle, - offset: u64, - size: u32, - flags: fuser::OpenFlags, - lock_owner: Option, - reply: fuser::ReplyData, - ) { - println!( - "read(ino={}, fh={}, offset={}, size={})", - ino, fh, offset, size - ); - - for (key, &inode) in self.inodes.iter() { - if inode == ino { - let value = self.tree.get(key).unwrap(); - reply.data(value.to_string().as_bytes()); - return; - } - } - - reply.error(Errno::ENOENT); - } -} diff --git a/src/local.rs b/src/local.rs index dd53707..4758a8d 100644 --- a/src/local.rs +++ b/src/local.rs @@ -3,10 +3,15 @@ use std::{ fs, io, os::unix::fs::{MetadataExt, PermissionsExt}, path::{Path, PathBuf}, + sync::{Arc, Mutex, mpsc}, + thread, time::{Duration, SystemTime}, }; use fuser::{Errno, FileAttr, Filesystem, Generation, INodeNo, Request}; +use notify::{Event, EventKind, RecursiveMode, Watcher}; + +use crate::file_watcher::FileWatcher; #[derive(PartialEq, Eq, Copy, Clone, Debug)] enum FileType { @@ -28,14 +33,14 @@ struct LocalItem { pub struct LocalOrigin { source: PathBuf, destination: PathBuf, - files: BTreeMap, + files: Arc>>, } impl LocalOrigin { pub fn new(source: String, destination: String) -> Result { println!("Initializing the LocalOrigin"); - let mut map: BTreeMap = BTreeMap::new(); + let map: Arc>> = Arc::new(Mutex::new(BTreeMap::new())); let local_root = LocalItem { inode: INodeNo::ROOT, @@ -45,9 +50,9 @@ impl LocalOrigin { file_type: FileType::Directory, metadata: fs::metadata(&source).unwrap(), }; - map.insert(INodeNo::ROOT, local_root); + map.lock().unwrap().insert(INodeNo::ROOT, local_root); - match LocalOrigin::read_source(PathBuf::from(&source), Path::new(&destination), &mut map) { + match LocalOrigin::read_source(Path::new(&source), Path::new(&destination), &map) { Ok(_) => { let local_origin = LocalOrigin { source: source.into(), @@ -62,10 +67,29 @@ impl LocalOrigin { } } - fn read_source( - source: PathBuf, + fn fill_fileset( + map: &Arc>>, + source: &Path, destination: &Path, - map: &mut BTreeMap, + ) { + let local_root = LocalItem { + inode: INodeNo::ROOT, + name: "/".to_string(), + original_path: source.to_path_buf(), + local_path: destination.to_path_buf(), + file_type: FileType::Directory, + metadata: fs::metadata(&source).unwrap(), + }; + map.lock().unwrap().insert(INodeNo::ROOT, local_root); + + let _ = LocalOrigin::read_source(&source, &destination, &map) + .inspect_err(|e| eprintln!("Error while reading source: {e}")); + } + + fn read_source( + source: &Path, + destination: &Path, + map: &Arc>>, ) -> Result<(), io::Error> { match fs::read_dir(source) { Ok(dir) => { @@ -95,10 +119,10 @@ impl LocalOrigin { }; let inode = local_item.inode; - map.insert(inode, local_item); + map.lock().unwrap().insert(inode, local_item); if file_type == FileType::Directory { - match LocalOrigin::read_source(item_path, destination, map) { + match LocalOrigin::read_source(&item_path, destination, map) { Ok(_) => {} Err(err) => return Err(err), }; @@ -155,11 +179,12 @@ impl Filesystem for LocalOrigin { &self, _req: &fuser::Request, ino: INodeNo, - fh: Option, + _fh: Option, reply: fuser::ReplyAttr, ) { println!("getattr(ino={})", ino); - match self.files.get(&ino) { + // TODO fix unwrap + match self.files.lock().unwrap().get(&ino) { Some(file) => { let ttl = Duration::new(1, 0); let attr = LocalOrigin::local_file_to_file_attr(file); @@ -195,7 +220,15 @@ impl Filesystem for LocalOrigin { ); } - for (i, (key, value)) in self.files.iter().skip(offset as usize).enumerate() { + // TODO fix unwrap + for (i, (key, value)) in self + .files + .lock() + .unwrap() + .iter() + .skip(offset as usize) + .enumerate() + { let offset: u64 = 2 + i as u64; let file_type: fuser::FileType = LocalOrigin::file_type(value.file_type); let path = &value.local_path; @@ -223,6 +256,8 @@ impl Filesystem for LocalOrigin { return match self .files + .lock() + .unwrap() .iter() .find(|item| item.1.name == name.to_str().unwrap()) { @@ -236,3 +271,54 @@ impl Filesystem for LocalOrigin { }; } } + +impl FileWatcher for LocalOrigin { + fn watch(&self) { + let files = self.files.clone(); + let source = self.source.clone(); + let destination = self.destination.clone(); + + println!("Starting to watch the source files in another thread"); + thread::spawn(move || { + let (tx, rx): ( + mpsc::Sender>, + mpsc::Receiver>, + ) = mpsc::channel(); + + // Use recommended_watcher() to automatically select the best implementation + // for your platform. The `EventHandler` passed to this constructor can be a + // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or + // another type the trait is implemented for. + let mut watcher: notify::INotifyWatcher = notify::recommended_watcher(tx).unwrap(); + + // Add a path to be watched. All files and directories at that path and + // below will be monitored for changes. + watcher.watch(&source, RecursiveMode::Recursive).unwrap(); + // Block forever, printing out events as they come in + for res in rx { + match res { + Ok(event) => { + println!("event: {:?}", event); + + match event.kind { + EventKind::Any => { + println!("Something happened to item, ignoring"); + } + EventKind::Access(_access_kind) => { + println!("Item was read"); + } + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => { + println!("Item was removed"); + LocalOrigin::fill_fileset(&files, &source, &destination); + } + EventKind::Other => { + println!("Some other action happened to item, ignoring"); + } + } + } + Err(e) => println!("watch error: {:?}", e), + } + } + }); + } +} diff --git a/src/main.rs b/src/main.rs index ed43d98..a1ce5ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ use clap::Parser; -use serde_json::json; -use std::env; +use file_watcher::FileWatcher; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; -mod json_filesystem; +mod file_watcher; mod local; #[derive(Parser, Debug)] @@ -33,7 +33,10 @@ fn main() { // TODO don't think clone is necessary // TODO unwrap might not be safe and better would be to handle it properly - let fs = local::LocalOrigin::new(args.source, mountpoint.clone()).unwrap(); + let fs = local::LocalOrigin::new(args.source.clone(), mountpoint.clone()).unwrap(); + + // start watching for changes in source files + fs.watch(); let cfg = fuser::Config::default(); let session = fuser::spawn_mount2(fs, &mountpoint, &cfg);