Handle updates of source files

This commit is contained in:
Alexander
2026-06-24 21:56:12 +02:00
parent f84061a30d
commit b51c680af4
9 changed files with 310 additions and 227 deletions
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
export GIT_CONFIG_GLOBAL=/dev/null
eval "$(devenv direnvrc)"
Generated
+197 -35
View File
@@ -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"
+1 -1
View File
@@ -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"
+1
View File
@@ -27,6 +27,7 @@
packages = with pkgs; [
git
just
opencode
];
outputs = {
+2
View File
@@ -0,0 +1,2 @@
build:
cargo build
+3
View File
@@ -0,0 +1,3 @@
pub trait FileWatcher {
fn watch(&self);
}
-175
View File
@@ -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<String, Value>,
attrs: BTreeMap<INodeNo, FileAttr>,
inodes: BTreeMap<String, INodeNo>,
}
impl JsonFilesystem {
pub fn new(tree: &Map<String, Value>) -> 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<fuser::FileHandle>,
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<fuser::LockOwner>,
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);
}
}
+98 -12
View File
@@ -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<INodeNo, LocalItem>,
files: Arc<Mutex<BTreeMap<INodeNo, LocalItem>>>,
}
impl LocalOrigin {
pub fn new(source: String, destination: String) -> Result<LocalOrigin, io::Error> {
println!("Initializing the LocalOrigin");
let mut map: BTreeMap<INodeNo, LocalItem> = BTreeMap::new();
let map: Arc<Mutex<BTreeMap<INodeNo, LocalItem>>> = 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<Mutex<BTreeMap<INodeNo, LocalItem>>>,
source: &Path,
destination: &Path,
map: &mut BTreeMap<INodeNo, LocalItem>,
) {
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<Mutex<BTreeMap<INodeNo, LocalItem>>>,
) -> 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<fuser::FileHandle>,
_fh: Option<fuser::FileHandle>,
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<Result<Event, notify::Error>>,
mpsc::Receiver<Result<Event, notify::Error>>,
) = 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),
}
}
});
}
}
+7 -4
View File
@@ -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);