From 76856b893a4249bc599e4fc13d8bb5a5fcdbd41b Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 12 May 2026 18:01:47 +0200 Subject: [PATCH] Implement Week 1 foundation: workspace, core types, FUSE skeleton, LocalOrigin - musicfs-core: OriginId, FileId, VirtualPath, ContentHash, AudioMeta, FileMeta, EventBus with FileAccessed event (5 tests) - musicfs-fuse: FUSE skeleton with EROFS handlers for write ops - musicfs-origins: Origin trait with watch(), LocalOrigin impl (6 tests) - flake.nix: Nix dev shell with rust toolchain, clang, lld, fuse3 All 11 tests pass. Build produces no warnings. --- musicfs/.cargo/config.toml | 10 + musicfs/.gitignore | 1 + musicfs/Cargo.lock | 791 ++++++++++++++++++ musicfs/Cargo.toml | 42 + musicfs/crates/musicfs-cache/Cargo.toml | 6 + musicfs/crates/musicfs-cache/src/lib.rs | 1 + musicfs/crates/musicfs-cas/Cargo.toml | 6 + musicfs/crates/musicfs-cas/src/lib.rs | 1 + musicfs/crates/musicfs-cli/Cargo.toml | 10 + musicfs/crates/musicfs-cli/src/lib.rs | 1 + musicfs/crates/musicfs-cli/src/main.rs | 3 + musicfs/crates/musicfs-core/Cargo.toml | 11 + musicfs/crates/musicfs-core/src/error.rs | 30 + musicfs/crates/musicfs-core/src/events.rs | 96 +++ musicfs/crates/musicfs-core/src/lib.rs | 7 + musicfs/crates/musicfs-core/src/types.rs | 179 ++++ musicfs/crates/musicfs-fuse/Cargo.toml | 11 + musicfs/crates/musicfs-fuse/src/filesystem.rs | 277 ++++++ musicfs/crates/musicfs-fuse/src/lib.rs | 3 + musicfs/crates/musicfs-grpc/Cargo.toml | 6 + musicfs/crates/musicfs-grpc/src/lib.rs | 1 + musicfs/crates/musicfs-metadata/Cargo.toml | 6 + musicfs/crates/musicfs-metadata/src/lib.rs | 1 + musicfs/crates/musicfs-origins/Cargo.toml | 13 + musicfs/crates/musicfs-origins/src/lib.rs | 5 + musicfs/crates/musicfs-origins/src/local.rs | 200 +++++ musicfs/crates/musicfs-origins/src/traits.rs | 55 ++ musicfs/crates/musicfs-plugins/Cargo.toml | 6 + musicfs/crates/musicfs-plugins/src/lib.rs | 1 + musicfs/crates/musicfs-search/Cargo.toml | 6 + musicfs/crates/musicfs-search/src/lib.rs | 1 + musicfs/crates/musicfs-sync/Cargo.toml | 6 + musicfs/crates/musicfs-sync/src/lib.rs | 1 + musicfs/flake.lock | 96 +++ musicfs/flake.nix | 43 + 35 files changed, 1933 insertions(+) create mode 100644 musicfs/.cargo/config.toml create mode 100644 musicfs/.gitignore create mode 100644 musicfs/Cargo.lock create mode 100644 musicfs/Cargo.toml create mode 100644 musicfs/crates/musicfs-cache/Cargo.toml create mode 100644 musicfs/crates/musicfs-cache/src/lib.rs create mode 100644 musicfs/crates/musicfs-cas/Cargo.toml create mode 100644 musicfs/crates/musicfs-cas/src/lib.rs create mode 100644 musicfs/crates/musicfs-cli/Cargo.toml create mode 100644 musicfs/crates/musicfs-cli/src/lib.rs create mode 100644 musicfs/crates/musicfs-cli/src/main.rs create mode 100644 musicfs/crates/musicfs-core/Cargo.toml create mode 100644 musicfs/crates/musicfs-core/src/error.rs create mode 100644 musicfs/crates/musicfs-core/src/events.rs create mode 100644 musicfs/crates/musicfs-core/src/lib.rs create mode 100644 musicfs/crates/musicfs-core/src/types.rs create mode 100644 musicfs/crates/musicfs-fuse/Cargo.toml create mode 100644 musicfs/crates/musicfs-fuse/src/filesystem.rs create mode 100644 musicfs/crates/musicfs-fuse/src/lib.rs create mode 100644 musicfs/crates/musicfs-grpc/Cargo.toml create mode 100644 musicfs/crates/musicfs-grpc/src/lib.rs create mode 100644 musicfs/crates/musicfs-metadata/Cargo.toml create mode 100644 musicfs/crates/musicfs-metadata/src/lib.rs create mode 100644 musicfs/crates/musicfs-origins/Cargo.toml create mode 100644 musicfs/crates/musicfs-origins/src/lib.rs create mode 100644 musicfs/crates/musicfs-origins/src/local.rs create mode 100644 musicfs/crates/musicfs-origins/src/traits.rs create mode 100644 musicfs/crates/musicfs-plugins/Cargo.toml create mode 100644 musicfs/crates/musicfs-plugins/src/lib.rs create mode 100644 musicfs/crates/musicfs-search/Cargo.toml create mode 100644 musicfs/crates/musicfs-search/src/lib.rs create mode 100644 musicfs/crates/musicfs-sync/Cargo.toml create mode 100644 musicfs/crates/musicfs-sync/src/lib.rs create mode 100644 musicfs/flake.lock create mode 100644 musicfs/flake.nix diff --git a/musicfs/.cargo/config.toml b/musicfs/.cargo/config.toml new file mode 100644 index 0000000..6f74a14 --- /dev/null +++ b/musicfs/.cargo/config.toml @@ -0,0 +1,10 @@ +[build] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] + +[target.x86_64-unknown-linux-gnu] +linker = "clang" + +[alias] +t = "test" +c = "check" +b = "build" diff --git a/musicfs/.gitignore b/musicfs/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/musicfs/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/musicfs/Cargo.lock b/musicfs/Cargo.lock new file mode 100644 index 0000000..4c7779a --- /dev/null +++ b/musicfs/Cargo.lock @@ -0,0 +1,791 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fuser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e697f6f62c20b6fad1ba0f84ae909f25971cf16e735273524e3977c94604cf8" +dependencies = [ + "libc", + "log", + "memchr", + "page_size", + "pkg-config", + "smallvec", + "zerocopy", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "musicfs-cache" +version = "0.1.0" + +[[package]] +name = "musicfs-cas" +version = "0.1.0" + +[[package]] +name = "musicfs-cli" +version = "0.1.0" + +[[package]] +name = "musicfs-core" +version = "0.1.0" +dependencies = [ + "hex", + "serde", + "thiserror", + "tokio", + "xxhash-rust", +] + +[[package]] +name = "musicfs-fuse" +version = "0.1.0" +dependencies = [ + "fuser", + "libc", + "musicfs-core", + "tokio", + "tracing", +] + +[[package]] +name = "musicfs-grpc" +version = "0.1.0" + +[[package]] +name = "musicfs-metadata" +version = "0.1.0" + +[[package]] +name = "musicfs-origins" +version = "0.1.0" +dependencies = [ + "async-trait", + "musicfs-core", + "tempfile", + "tokio", + "tracing", +] + +[[package]] +name = "musicfs-plugins" +version = "0.1.0" + +[[package]] +name = "musicfs-search" +version = "0.1.0" + +[[package]] +name = "musicfs-sync" +version = "0.1.0" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/musicfs/Cargo.toml b/musicfs/Cargo.toml new file mode 100644 index 0000000..9b9d4f8 --- /dev/null +++ b/musicfs/Cargo.toml @@ -0,0 +1,42 @@ +[workspace] +resolver = "2" +members = ["crates/*"] + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +rust-version = "1.75" +authors = ["MusicFS Contributors"] +repository = "https://github.com/user/musicfs" + +[workspace.dependencies] +# Async runtime +tokio = { version = "1", features = ["full"] } +async-trait = "0.1" + +# Error handling +thiserror = "1" +anyhow = "1" + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rmp-serde = "1" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# FUSE +fuser = "0.14" + +# Database +rusqlite = { version = "0.31", features = ["bundled"] } +sled = "0.34" + +# Hashing (per architecture 8.3) +xxhash-rust = { version = "0.8", features = ["xxh64"] } + +# Testing +tempfile = "3" diff --git a/musicfs/crates/musicfs-cache/Cargo.toml b/musicfs/crates/musicfs-cache/Cargo.toml new file mode 100644 index 0000000..1df036d --- /dev/null +++ b/musicfs/crates/musicfs-cache/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "musicfs-cache" +version.workspace = true +edition.workspace = true + +[dependencies] diff --git a/musicfs/crates/musicfs-cache/src/lib.rs b/musicfs/crates/musicfs-cache/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-cache/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/crates/musicfs-cas/Cargo.toml b/musicfs/crates/musicfs-cas/Cargo.toml new file mode 100644 index 0000000..1021bb7 --- /dev/null +++ b/musicfs/crates/musicfs-cas/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "musicfs-cas" +version.workspace = true +edition.workspace = true + +[dependencies] diff --git a/musicfs/crates/musicfs-cas/src/lib.rs b/musicfs/crates/musicfs-cas/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-cas/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/crates/musicfs-cli/Cargo.toml b/musicfs/crates/musicfs-cli/Cargo.toml new file mode 100644 index 0000000..8e52f26 --- /dev/null +++ b/musicfs/crates/musicfs-cli/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "musicfs-cli" +version.workspace = true +edition.workspace = true + +[[bin]] +name = "musicfs" +path = "src/main.rs" + +[dependencies] diff --git a/musicfs/crates/musicfs-cli/src/lib.rs b/musicfs/crates/musicfs-cli/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-cli/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/crates/musicfs-cli/src/main.rs b/musicfs/crates/musicfs-cli/src/main.rs new file mode 100644 index 0000000..d0c1637 --- /dev/null +++ b/musicfs/crates/musicfs-cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("MusicFS CLI - placeholder"); +} diff --git a/musicfs/crates/musicfs-core/Cargo.toml b/musicfs/crates/musicfs-core/Cargo.toml new file mode 100644 index 0000000..6535876 --- /dev/null +++ b/musicfs/crates/musicfs-core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "musicfs-core" +version.workspace = true +edition.workspace = true + +[dependencies] +thiserror.workspace = true +serde.workspace = true +tokio = { workspace = true, features = ["sync"] } +xxhash-rust.workspace = true +hex = "0.4" diff --git a/musicfs/crates/musicfs-core/src/error.rs b/musicfs/crates/musicfs-core/src/error.rs new file mode 100644 index 0000000..6d32ad1 --- /dev/null +++ b/musicfs/crates/musicfs-core/src/error.rs @@ -0,0 +1,30 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Origin not found: {0}")] + OriginNotFound(String), + + #[error("File not found: {0}")] + FileNotFound(String), + + #[error("Path resolution failed: {0}")] + PathResolution(String), + + #[error("Cache error: {0}")] + Cache(String), + + #[error("Database error: {0}")] + Database(String), + + #[error("NFS stale file handle")] + NfsStaleHandle, + + #[error("Operation not permitted (read-only filesystem)")] + ReadOnly, +} + +pub type Result = std::result::Result; diff --git a/musicfs/crates/musicfs-core/src/events.rs b/musicfs/crates/musicfs-core/src/events.rs new file mode 100644 index 0000000..e4c3aac --- /dev/null +++ b/musicfs/crates/musicfs-core/src/events.rs @@ -0,0 +1,96 @@ +use crate::types::{OriginId, VirtualPath}; +use tokio::sync::broadcast; + +pub struct EventBus { + sender: broadcast::Sender, +} + +impl EventBus { + pub fn new(capacity: usize) -> Self { + let (sender, _) = broadcast::channel(capacity); + Self { sender } + } + + pub fn publish(&self, event: Event) { + let _ = self.sender.send(event); + } + + pub fn subscribe(&self) -> broadcast::Receiver { + self.sender.subscribe() + } +} + +impl Default for EventBus { + fn default() -> Self { + Self::new(1024) + } +} + +#[derive(Clone, Debug)] +pub enum Event { + FileAdded { + path: VirtualPath, + origin_id: OriginId, + }, + FileRemoved { + path: VirtualPath, + }, + FileModified { + path: VirtualPath, + }, + FileAccessed { + path: VirtualPath, + origin_id: OriginId, + offset: u64, + size: u32, + }, + OriginConnected { + origin_id: OriginId, + }, + OriginDisconnected { + origin_id: OriginId, + }, + SyncStarted { + origin_id: OriginId, + }, + SyncCompleted { + origin_id: OriginId, + files_changed: u64, + }, + CacheEviction { + bytes_freed: u64, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_event_bus() { + let bus = EventBus::new(16); + let mut rx = bus.subscribe(); + + bus.publish(Event::SyncStarted { + origin_id: OriginId::from("test"), + }); + + let event = rx.recv().await.unwrap(); + assert!(matches!(event, Event::SyncStarted { .. })); + } + + #[tokio::test] + async fn test_event_bus_multiple_subscribers() { + let bus = EventBus::new(16); + let mut rx1 = bus.subscribe(); + let mut rx2 = bus.subscribe(); + + bus.publish(Event::CacheEviction { bytes_freed: 1024 }); + + let e1 = rx1.recv().await.unwrap(); + let e2 = rx2.recv().await.unwrap(); + + assert!(matches!(e1, Event::CacheEviction { bytes_freed: 1024 })); + assert!(matches!(e2, Event::CacheEviction { bytes_freed: 1024 })); + } +} diff --git a/musicfs/crates/musicfs-core/src/lib.rs b/musicfs/crates/musicfs-core/src/lib.rs new file mode 100644 index 0000000..02d5d5f --- /dev/null +++ b/musicfs/crates/musicfs-core/src/lib.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod events; +pub mod types; + +pub use error::{Error, Result}; +pub use events::{Event, EventBus}; +pub use types::*; diff --git a/musicfs/crates/musicfs-core/src/types.rs b/musicfs/crates/musicfs-core/src/types.rs new file mode 100644 index 0000000..2a9e7e1 --- /dev/null +++ b/musicfs/crates/musicfs-core/src/types.rs @@ -0,0 +1,179 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::time::SystemTime; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct OriginId(pub String); + +impl From<&str> for OriginId { + fn from(s: &str) -> Self { + Self(s.to_string()) + } +} + +impl std::fmt::Display for OriginId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct FileId(pub i64); + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VirtualPath(pub PathBuf); + +impl VirtualPath { + pub fn new(path: impl Into) -> Self { + Self(path.into()) + } + + pub fn as_path(&self) -> &std::path::Path { + &self.0 + } + + pub fn as_str(&self) -> &str { + self.0.to_str().unwrap_or("") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RealPath { + pub origin_id: OriginId, + pub path: PathBuf, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ContentHash(pub [u8; 8]); + +impl ContentHash { + pub fn from_bytes(data: &[u8]) -> Self { + use xxhash_rust::xxh64::xxh64; + Self(xxh64(data, 0).to_le_bytes()) + } + + pub fn to_hex(&self) -> String { + hex::encode(self.0) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ChunkHash(pub [u8; 8]); + +impl ChunkHash { + pub fn from_bytes(data: &[u8]) -> Self { + use xxhash_rust::xxh64::xxh64; + Self(xxh64(data, 0).to_le_bytes()) + } + + pub fn to_hex(&self) -> String { + hex::encode(self.0) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum AudioFormat { + Flac, + Mp3, + Opus, + Vorbis, + Aac, + Alac, + Wav, + #[default] + Unknown, +} + +impl AudioFormat { + pub fn from_extension(ext: &str) -> Self { + match ext.to_lowercase().as_str() { + "flac" => Self::Flac, + "mp3" => Self::Mp3, + "opus" => Self::Opus, + "ogg" => Self::Vorbis, + "m4a" | "aac" => Self::Aac, + "wav" => Self::Wav, + _ => Self::Unknown, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct AudioMeta { + pub title: Option, + pub artist: Option, + pub album: Option, + pub album_artist: Option, + pub genre: Option, + pub year: Option, + pub track: Option, + pub disc: Option, + pub duration_ms: Option, + pub bitrate: Option, + pub sample_rate: Option, + pub format: AudioFormat, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileMeta { + pub id: FileId, + pub virtual_path: VirtualPath, + pub real_path: RealPath, + pub size: u64, + pub mtime: SystemTime, + pub content_hash: Option, + pub audio: Option, +} + +#[derive(Debug, Clone)] +pub struct DirEntry { + pub name: String, + pub is_dir: bool, + pub size: u64, + pub mtime: SystemTime, +} + +#[derive(Debug, Clone)] +pub struct FileStat { + pub size: u64, + pub mtime: SystemTime, + pub is_dir: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum HealthStatus { + Healthy, + Degraded, + Unhealthy, + #[default] + Unknown, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_content_hash() { + let data = b"hello world"; + let hash1 = ContentHash::from_bytes(data); + let hash2 = ContentHash::from_bytes(data); + assert_eq!(hash1, hash2); + + let hash3 = ContentHash::from_bytes(b"different"); + assert_ne!(hash1, hash3); + } + + #[test] + fn test_audio_format_from_extension() { + assert_eq!(AudioFormat::from_extension("flac"), AudioFormat::Flac); + assert_eq!(AudioFormat::from_extension("MP3"), AudioFormat::Mp3); + assert_eq!(AudioFormat::from_extension("unknown"), AudioFormat::Unknown); + } + + #[test] + fn test_virtual_path() { + let path = VirtualPath::new("/Artist/Album/Track.flac"); + assert_eq!(path.as_str(), "/Artist/Album/Track.flac"); + } +} diff --git a/musicfs/crates/musicfs-fuse/Cargo.toml b/musicfs/crates/musicfs-fuse/Cargo.toml new file mode 100644 index 0000000..7ac2d2f --- /dev/null +++ b/musicfs/crates/musicfs-fuse/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "musicfs-fuse" +version.workspace = true +edition.workspace = true + +[dependencies] +musicfs-core = { path = "../musicfs-core" } +fuser.workspace = true +tokio.workspace = true +tracing.workspace = true +libc = "0.2" diff --git a/musicfs/crates/musicfs-fuse/src/filesystem.rs b/musicfs/crates/musicfs-fuse/src/filesystem.rs new file mode 100644 index 0000000..89c69ae --- /dev/null +++ b/musicfs/crates/musicfs-fuse/src/filesystem.rs @@ -0,0 +1,277 @@ +use fuser::{ + FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen, + Request, FUSE_ROOT_ID, +}; +use musicfs_core::{Error, Result}; +use std::ffi::OsStr; +use std::path::Path; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tracing::{debug, info}; + +const TTL: Duration = Duration::from_secs(1); + +pub struct MusicFs { + uid: u32, + gid: u32, +} + +impl MusicFs { + pub fn new() -> Self { + Self { + uid: unsafe { libc::getuid() }, + gid: unsafe { libc::getgid() }, + } + } + + pub fn mount(self, mountpoint: &Path) -> Result<()> { + info!("Mounting MusicFS at {:?}", mountpoint); + + let options = vec![ + fuser::MountOption::RO, + fuser::MountOption::FSName("musicfs".to_string()), + fuser::MountOption::AutoUnmount, + fuser::MountOption::AllowOther, + ]; + + fuser::mount2(self, mountpoint, &options).map_err(Error::Io)?; + + Ok(()) + } + + fn root_attr(&self) -> FileAttr { + FileAttr { + ino: FUSE_ROOT_ID, + size: 0, + blocks: 0, + atime: UNIX_EPOCH, + mtime: UNIX_EPOCH, + ctime: UNIX_EPOCH, + crtime: UNIX_EPOCH, + kind: FileType::Directory, + perm: 0o755, + nlink: 2, + uid: self.uid, + gid: self.gid, + rdev: 0, + blksize: 512, + flags: 0, + } + } +} + +impl Default for MusicFs { + fn default() -> Self { + Self::new() + } +} + +impl Filesystem for MusicFs { + fn init( + &mut self, + _req: &Request<'_>, + _config: &mut fuser::KernelConfig, + ) -> std::result::Result<(), libc::c_int> { + info!("MusicFS initialized"); + Ok(()) + } + + fn destroy(&mut self) { + info!("MusicFS destroyed"); + } + + fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { + debug!("lookup(parent={}, name={:?})", parent, name); + reply.error(libc::ENOENT); + } + + fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { + debug!("getattr(ino={})", ino); + + if ino == FUSE_ROOT_ID { + reply.attr(&TTL, &self.root_attr()); + } else { + reply.error(libc::ENOENT); + } + } + + fn readdir( + &mut self, + _req: &Request, + ino: u64, + _fh: u64, + offset: i64, + mut reply: ReplyDirectory, + ) { + debug!("readdir(ino={}, offset={})", ino, offset); + + if ino == FUSE_ROOT_ID { + if offset == 0 { + let _ = reply.add(FUSE_ROOT_ID, 1, FileType::Directory, "."); + } + if offset <= 1 { + let _ = reply.add(FUSE_ROOT_ID, 2, FileType::Directory, ".."); + } + reply.ok(); + } else { + reply.error(libc::ENOENT); + } + } + + fn open(&mut self, _req: &Request, ino: u64, flags: i32, reply: ReplyOpen) { + debug!("open(ino={}, flags={})", ino, flags); + + let write_flags = libc::O_WRONLY | libc::O_RDWR | libc::O_APPEND | libc::O_TRUNC; + if flags & write_flags != 0 { + reply.error(libc::EROFS); + return; + } + + reply.error(libc::ENOENT); + } + + fn read( + &mut self, + _req: &Request, + ino: u64, + _fh: u64, + offset: i64, + size: u32, + _flags: i32, + _lock_owner: Option, + reply: ReplyData, + ) { + debug!("read(ino={}, offset={}, size={})", ino, offset, size); + reply.error(libc::ENOENT); + } + + fn release( + &mut self, + _req: &Request, + ino: u64, + _fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: fuser::ReplyEmpty, + ) { + debug!("release(ino={})", ino); + reply.ok(); + } + + fn write( + &mut self, + _req: &Request, + _ino: u64, + _fh: u64, + _offset: i64, + _data: &[u8], + _write_flags: u32, + _flags: i32, + _lock_owner: Option, + reply: fuser::ReplyWrite, + ) { + reply.error(libc::EROFS); + } + + fn mkdir( + &mut self, + _req: &Request, + _parent: u64, + _name: &OsStr, + _mode: u32, + _umask: u32, + reply: ReplyEntry, + ) { + reply.error(libc::EROFS); + } + + fn unlink(&mut self, _req: &Request, _parent: u64, _name: &OsStr, reply: fuser::ReplyEmpty) { + reply.error(libc::EROFS); + } + + fn rmdir(&mut self, _req: &Request, _parent: u64, _name: &OsStr, reply: fuser::ReplyEmpty) { + reply.error(libc::EROFS); + } + + fn rename( + &mut self, + _req: &Request, + _parent: u64, + _name: &OsStr, + _newparent: u64, + _newname: &OsStr, + _flags: u32, + reply: fuser::ReplyEmpty, + ) { + reply.error(libc::EROFS); + } + + fn create( + &mut self, + _req: &Request, + _parent: u64, + _name: &OsStr, + _mode: u32, + _umask: u32, + _flags: i32, + reply: fuser::ReplyCreate, + ) { + reply.error(libc::EROFS); + } + + fn setattr( + &mut self, + _req: &Request, + _ino: u64, + _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, + ) { + reply.error(libc::EROFS); + } + + fn symlink( + &mut self, + _req: &Request, + _parent: u64, + _name: &OsStr, + _link: &Path, + reply: ReplyEntry, + ) { + reply.error(libc::EROFS); + } + + fn link( + &mut self, + _req: &Request, + _ino: u64, + _newparent: u64, + _newname: &OsStr, + reply: ReplyEntry, + ) { + reply.error(libc::EROFS); + } + + fn mknod( + &mut self, + _req: &Request, + _parent: u64, + _name: &OsStr, + _mode: u32, + _umask: u32, + _rdev: u32, + reply: ReplyEntry, + ) { + reply.error(libc::EROFS); + } +} diff --git a/musicfs/crates/musicfs-fuse/src/lib.rs b/musicfs/crates/musicfs-fuse/src/lib.rs new file mode 100644 index 0000000..f26c05f --- /dev/null +++ b/musicfs/crates/musicfs-fuse/src/lib.rs @@ -0,0 +1,3 @@ +mod filesystem; + +pub use filesystem::MusicFs; diff --git a/musicfs/crates/musicfs-grpc/Cargo.toml b/musicfs/crates/musicfs-grpc/Cargo.toml new file mode 100644 index 0000000..c249865 --- /dev/null +++ b/musicfs/crates/musicfs-grpc/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "musicfs-grpc" +version.workspace = true +edition.workspace = true + +[dependencies] diff --git a/musicfs/crates/musicfs-grpc/src/lib.rs b/musicfs/crates/musicfs-grpc/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-grpc/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/crates/musicfs-metadata/Cargo.toml b/musicfs/crates/musicfs-metadata/Cargo.toml new file mode 100644 index 0000000..aa5107f --- /dev/null +++ b/musicfs/crates/musicfs-metadata/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "musicfs-metadata" +version.workspace = true +edition.workspace = true + +[dependencies] diff --git a/musicfs/crates/musicfs-metadata/src/lib.rs b/musicfs/crates/musicfs-metadata/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-metadata/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/crates/musicfs-origins/Cargo.toml b/musicfs/crates/musicfs-origins/Cargo.toml new file mode 100644 index 0000000..112c0da --- /dev/null +++ b/musicfs/crates/musicfs-origins/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "musicfs-origins" +version.workspace = true +edition.workspace = true + +[dependencies] +musicfs-core = { path = "../musicfs-core" } +async-trait.workspace = true +tokio = { workspace = true, features = ["fs", "sync"] } +tracing.workspace = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/musicfs/crates/musicfs-origins/src/lib.rs b/musicfs/crates/musicfs-origins/src/lib.rs new file mode 100644 index 0000000..f15d6e0 --- /dev/null +++ b/musicfs/crates/musicfs-origins/src/lib.rs @@ -0,0 +1,5 @@ +mod local; +mod traits; + +pub use local::LocalOrigin; +pub use traits::{Origin, OriginType, WatchCallback, WatchEvent, WatchHandle}; diff --git a/musicfs/crates/musicfs-origins/src/local.rs b/musicfs/crates/musicfs-origins/src/local.rs new file mode 100644 index 0000000..fe4f0b4 --- /dev/null +++ b/musicfs/crates/musicfs-origins/src/local.rs @@ -0,0 +1,200 @@ +use crate::traits::{Origin, OriginType, WatchCallback, WatchHandle}; +use async_trait::async_trait; +use musicfs_core::{DirEntry, FileStat, HealthStatus, OriginId, Result}; +use std::path::{Path, PathBuf}; +use tokio::fs; +use tokio::io::AsyncRead; +use tracing::debug; + +pub struct LocalOrigin { + id: OriginId, + root: PathBuf, + display_name: String, +} + +impl LocalOrigin { + pub fn new(id: impl Into, root: impl Into) -> Self { + let root = root.into(); + let display_name = format!("Local: {}", root.display()); + Self { + id: id.into(), + root, + display_name, + } + } + + fn full_path(&self, path: &Path) -> PathBuf { + if path.as_os_str().is_empty() || path == Path::new("/") { + self.root.clone() + } else { + self.root.join(path.strip_prefix("/").unwrap_or(path)) + } + } +} + +#[async_trait] +impl Origin for LocalOrigin { + fn id(&self) -> &OriginId { + &self.id + } + + fn origin_type(&self) -> OriginType { + OriginType::Local + } + + fn display_name(&self) -> &str { + &self.display_name + } + + async fn readdir(&self, path: &Path) -> Result> { + let full_path = self.full_path(path); + debug!("LocalOrigin::readdir({:?})", full_path); + + let mut entries = Vec::new(); + let mut dir = fs::read_dir(&full_path).await?; + + while let Some(entry) = dir.next_entry().await? { + let metadata = entry.metadata().await?; + let name = entry.file_name().to_string_lossy().into_owned(); + + entries.push(DirEntry { + name, + is_dir: metadata.is_dir(), + size: metadata.len(), + mtime: metadata.modified().unwrap_or(std::time::UNIX_EPOCH), + }); + } + + Ok(entries) + } + + async fn stat(&self, path: &Path) -> Result { + let full_path = self.full_path(path); + debug!("LocalOrigin::stat({:?})", full_path); + + let metadata = fs::metadata(&full_path).await?; + + Ok(FileStat { + size: metadata.len(), + mtime: metadata.modified().unwrap_or(std::time::UNIX_EPOCH), + is_dir: metadata.is_dir(), + }) + } + + async fn read(&self, path: &Path, offset: u64, size: u32) -> Result> { + use tokio::io::{AsyncReadExt, AsyncSeekExt}; + + let full_path = self.full_path(path); + debug!( + "LocalOrigin::read({:?}, offset={}, size={})", + full_path, offset, size + ); + + let mut file = fs::File::open(&full_path).await?; + file.seek(std::io::SeekFrom::Start(offset)).await?; + + let mut buffer = vec![0u8; size as usize]; + let bytes_read = file.read(&mut buffer).await?; + buffer.truncate(bytes_read); + + Ok(buffer) + } + + async fn exists(&self, path: &Path) -> Result { + let full_path = self.full_path(path); + Ok(fs::try_exists(&full_path).await?) + } + + async fn health(&self) -> HealthStatus { + match fs::try_exists(&self.root).await { + Ok(true) => HealthStatus::Healthy, + Ok(false) => HealthStatus::Unhealthy, + Err(_) => HealthStatus::Unhealthy, + } + } + + async fn open_read(&self, path: &Path) -> Result> { + let full_path = self.full_path(path); + let file = fs::File::open(&full_path).await?; + Ok(Box::new(file)) + } + + async fn watch(&self, path: &Path, _callback: WatchCallback) -> Result { + debug!("LocalOrigin::watch({:?}) - stub implementation", path); + let (tx, _rx) = tokio::sync::oneshot::channel(); + Ok(WatchHandle::new(tx)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[tokio::test] + async fn test_local_origin_readdir() { + let dir = TempDir::new().unwrap(); + std::fs::write(dir.path().join("test.txt"), "hello").unwrap(); + std::fs::create_dir(dir.path().join("subdir")).unwrap(); + + let origin = LocalOrigin::new("test", dir.path()); + let entries = origin.readdir(Path::new("/")).await.unwrap(); + + assert_eq!(entries.len(), 2); + assert!(entries.iter().any(|e| e.name == "test.txt" && !e.is_dir)); + assert!(entries.iter().any(|e| e.name == "subdir" && e.is_dir)); + } + + #[tokio::test] + async fn test_local_origin_stat() { + let dir = TempDir::new().unwrap(); + std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); + + let origin = LocalOrigin::new("test", dir.path()); + let stat = origin.stat(Path::new("/test.txt")).await.unwrap(); + + assert_eq!(stat.size, 11); + assert!(!stat.is_dir); + } + + #[tokio::test] + async fn test_local_origin_read() { + let dir = TempDir::new().unwrap(); + std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); + + let origin = LocalOrigin::new("test", dir.path()); + let data = origin.read(Path::new("/test.txt"), 0, 5).await.unwrap(); + + assert_eq!(data, b"hello"); + } + + #[tokio::test] + async fn test_local_origin_read_offset() { + let dir = TempDir::new().unwrap(); + std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); + + let origin = LocalOrigin::new("test", dir.path()); + let data = origin.read(Path::new("/test.txt"), 6, 5).await.unwrap(); + + assert_eq!(data, b"world"); + } + + #[tokio::test] + async fn test_local_origin_exists() { + let dir = TempDir::new().unwrap(); + std::fs::write(dir.path().join("test.txt"), "hello").unwrap(); + + let origin = LocalOrigin::new("test", dir.path()); + + assert!(origin.exists(Path::new("/test.txt")).await.unwrap()); + assert!(!origin.exists(Path::new("/nonexistent.txt")).await.unwrap()); + } + + #[tokio::test] + async fn test_local_origin_health() { + let dir = TempDir::new().unwrap(); + let origin = LocalOrigin::new("test", dir.path()); + + assert_eq!(origin.health().await, HealthStatus::Healthy); + } +} diff --git a/musicfs/crates/musicfs-origins/src/traits.rs b/musicfs/crates/musicfs-origins/src/traits.rs new file mode 100644 index 0000000..c367685 --- /dev/null +++ b/musicfs/crates/musicfs-origins/src/traits.rs @@ -0,0 +1,55 @@ +use async_trait::async_trait; +use musicfs_core::{DirEntry, FileStat, HealthStatus, OriginId, Result}; +use std::path::{Path, PathBuf}; +use tokio::io::AsyncRead; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OriginType { + Local, + Nfs, + Smb, + S3, + Sftp, +} + +#[async_trait] +pub trait Origin: Send + Sync { + fn id(&self) -> &OriginId; + + fn origin_type(&self) -> OriginType; + + fn display_name(&self) -> &str; + + async fn readdir(&self, path: &Path) -> Result>; + + async fn stat(&self, path: &Path) -> Result; + + async fn read(&self, path: &Path, offset: u64, size: u32) -> Result>; + + async fn exists(&self, path: &Path) -> Result; + + async fn health(&self) -> HealthStatus; + + async fn open_read(&self, path: &Path) -> Result>; + + async fn watch(&self, path: &Path, callback: WatchCallback) -> Result; +} + +pub type WatchCallback = Box; + +pub struct WatchHandle { + _cancel: tokio::sync::oneshot::Sender<()>, +} + +impl WatchHandle { + pub fn new(cancel: tokio::sync::oneshot::Sender<()>) -> Self { + Self { _cancel: cancel } + } +} + +#[derive(Debug, Clone)] +pub enum WatchEvent { + Created(PathBuf), + Modified(PathBuf), + Deleted(PathBuf), +} diff --git a/musicfs/crates/musicfs-plugins/Cargo.toml b/musicfs/crates/musicfs-plugins/Cargo.toml new file mode 100644 index 0000000..c1e3ace --- /dev/null +++ b/musicfs/crates/musicfs-plugins/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "musicfs-plugins" +version.workspace = true +edition.workspace = true + +[dependencies] diff --git a/musicfs/crates/musicfs-plugins/src/lib.rs b/musicfs/crates/musicfs-plugins/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-plugins/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/crates/musicfs-search/Cargo.toml b/musicfs/crates/musicfs-search/Cargo.toml new file mode 100644 index 0000000..d8b8139 --- /dev/null +++ b/musicfs/crates/musicfs-search/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "musicfs-search" +version.workspace = true +edition.workspace = true + +[dependencies] diff --git a/musicfs/crates/musicfs-search/src/lib.rs b/musicfs/crates/musicfs-search/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-search/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/crates/musicfs-sync/Cargo.toml b/musicfs/crates/musicfs-sync/Cargo.toml new file mode 100644 index 0000000..9e7c3cb --- /dev/null +++ b/musicfs/crates/musicfs-sync/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "musicfs-sync" +version.workspace = true +edition.workspace = true + +[dependencies] diff --git a/musicfs/crates/musicfs-sync/src/lib.rs b/musicfs/crates/musicfs-sync/src/lib.rs new file mode 100644 index 0000000..f9da2c4 --- /dev/null +++ b/musicfs/crates/musicfs-sync/src/lib.rs @@ -0,0 +1 @@ +#![allow(dead_code)] diff --git a/musicfs/flake.lock b/musicfs/flake.lock new file mode 100644 index 0000000..e93f5ce --- /dev/null +++ b/musicfs/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1778443072, + "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1778555852, + "narHash": "sha256-55EmwooVAS4UpA0oWd5wilKPRqCiHD5BAej9QiNwheY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "f29b0f7a9f367e0056b716f8aa137cb41e784444", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/musicfs/flake.nix b/musicfs/flake.nix new file mode 100644 index 0000000..f0b2b42 --- /dev/null +++ b/musicfs/flake.nix @@ -0,0 +1,43 @@ +{ + description = "MusicFS - FUSE filesystem for music libraries"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ]; + }; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + pkg-config + fuse3 + sqlite + openssl + + # Linker toolchain + clang + lld + + # Dev tools + cargo-watch + cargo-nextest + ]; + + RUST_BACKTRACE = "1"; + RUST_LOG = "debug"; + }; + } + ); +}