#+title: musicfs A read-only FUSE filesystem that presents a music library reorganised by *metadata* rather than by however the files happen to sit on disk. Point it at a messy ~~/Music~ tree (or a remote ~musicfs-server~) and it mounts a clean =Artist/Album/Track= view, driven entirely by the tags parsed out of the audio files themselves. #+begin_example mountpoint/ ├── ДДТ/ │ └── Творчество в пустоте/ │ ├── 01 Intro.flac │ └── 02 ... └── Some Artist/ └── Some Album/ └── 01 Track.flac #+end_example The source files are never moved or modified. The directory hierarchy is *virtual*: synthesised from ~album_artist~/~album~ tags, with stable synthetic inodes so the layout survives remounts. * How it works Two *origins* feed the same FUSE frontend: - *LocalOrigin* — walks a directory on the host, parses tags, builds the snapshot. A =notify= watcher keeps it live as files change. - *NetworkOrigin* — talks gRPC to a remote ~musicfs-server~. Metadata and file bytes arrive over the wire; a Postgres table caches both. Reconciliation is hash-based: the client sends the ~(inode, hash)~ pairs it has, the server replies with only what changed or was deleted, so almost nothing crosses the network on a steady-state refresh. Which one is used is chosen automatically from the ~--source~ argument: an =http(s)://= URL means network, anything else is treated as a local path. State lives in Postgres — parsed metadata, the virtual-path layout, and (for the network origin) a lazy byte cache populated only for files that were actually read. * Layout | Crate | Responsibility | |------------------+-----------------------------------------------------------------------| | =musicfs-proto= | Protobuf/gRPC definitions (=proto/musicfs.proto=), generated bindings | | =musicfs-core= | Shared logic: tag parsing (FLAC/MP3 via symphonia), hashing, logging | | =musicfs-client= | FUSE mount, origins, Postgres cache/sync — the =musicfs= binary | | =musicfs-server= | gRPC server sharing a library — the =musicfs-server= binary | The gRPC surface (see =proto/musicfs.proto=): ~Reconcile~, ~GetMetadata~, ~GetManifest~, ~GetFile~ (whole-file or byte-range streaming), and ~SubscribeEvents~ (change wake-ups). The client also serves a ~ClientStatus~ RPC for introspection. * Running the client Everything runs inside the =devenv= shell, which provides the Rust toolchain, Postgres, FUSE tooling, and gRPC utilities. #+begin_src bash devenv shell devenv up --profile local # starts Postgres + the FUSE mount #+end_src Predefined profiles in =devenv.nix=: | Profile | Source | Purpose | |-----------+---------------------------------+-------------------------------| | =local= | a local directory | mount a host music folder | | =remote= | =http://…:50051= | mount a remote musicfs-server | | =e2e= | =http://127.0.0.1:50061= | end-to-end test harness | Or run the binary directly: #+begin_src bash cargo run -p musicfs-client -- \ --source /home/you/Music \ --mountpoint /tmp/musicfs \ --database "postgresql://you@localhost/musicfs?host=$PGHOST" #+end_src Key flags: =--source= (local path or ~http(s)://~ server URL), =--mountpoint=, =--database= (Postgres URL), =--log-dir= (daily-rotated logs, default =./logs=), =--listen= (address for the client's status/health RPCs, default =127.0.0.1:50052=). * Running the server in an Incus VM ~musicfs-server~ (gRPC) runs inside an Incus VM, live-sharing the host's ~~/Music~ as a read-only virtiofs mount. =scripts/vm.sh= builds the binary on the host, bundles its nix ~glibc~ so it runs unmodified in the VM, ships it in, and runs it under systemd. #+begin_src bash devenv shell # cargo, incus, patchelf, grpcurl scripts/vm.sh up # create VM, share ~/Music, build, ship, start server #+end_src Options (env): =VM_NAME=, =IMAGE=, =MUSIC_SOURCE=, =LISTEN_PORT=, =RUST_LOG=. | Command | Action | |------------------------+-------------------------------| | =scripts/vm.sh up= | create + build + ship + start | | =scripts/vm.sh redeploy= | rebuild after a code change | | =scripts/vm.sh logs= | tail server logs | | =scripts/vm.sh status= | VM + service status | | =scripts/vm.sh shell= | shell inside the VM | | =scripts/vm.sh down= | stop the VM | | =scripts/vm.sh destroy= | delete the VM | Reach the server at the VM's bridge IP (=scripts/vm.sh status= prints it). ** Poke at it (Nushell) #+begin_src nushell let VMIP = (incus list musicfs -f csv -c 4 | lines | first | split row " " | first) let ADDR = $"($VMIP):50051" # liveness ^nc -z -w 3 $VMIP 50051 if $env.LAST_EXIT_CODE == 0 { print "alive" } else { print "dead" } # gRPC: stream the manifest, count entries grpcurl -plaintext -import-path proto -proto musicfs.proto -d "{}" $ADDR musicfs.MusicFs/GetManifest | lines | find relPath | length # files the server scans incus exec musicfs -- find /music -type f | lines | length # play a track straight out of the VM's shared /music incus exec musicfs -- cat "/music/DDT/ДДТ - Творчество в пустоте – 2 - 01 Intro.flac" | ^mpv - #+end_src Install mpv with =nix profile install nixpkgs#mpv=, or use =^ffplay -= instead. In Nushell, always put =| lines= between an external command's stdout and a builtin (=find=, =first=, =length=, …); external-to-external pipes (=cat | mpv=) are byte-stable and don't need it. * Development #+begin_src bash just build # cargo build cargo test # unit tests just e2e # end-to-end suite (scripts/e2e/run.sh) just e2e-resilience # resilience suite #+end_src Formatting and lint (clippy, treefmt with rustfmt + nixfmt) run as git hooks via =devenv=. Requires Rust edition 2024.