Add CLI implementation and MVP performance review

- Implement functional CLI with clap argument parsing
- Add directory scanning and metadata extraction at startup
- Fix filesystem.rs to store tokio Handle for async/sync bridge
- Fix flake.nix with LD_LIBRARY_PATH for libfuse3
- Add MVP performance review with real-world benchmark results

Benchmarks show:
- Mount time: 8ms (target <500ms)
- Throughput: 2-3 GB/s (target >500 MB/s)
- Identifies critical gap: incomplete file caching (only ~2MB per file)
- Identifies missing CDC chunking per architecture spec
This commit is contained in:
Alexander
2026-05-12 19:28:13 +02:00
parent c46750b1ec
commit 7ad554f8d5
7 changed files with 698 additions and 11 deletions
+232
View File
@@ -14,6 +14,65 @@ dependencies = [
"zerocopy 0.8.48",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.102"
@@ -89,6 +148,52 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -325,6 +430,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
@@ -390,6 +501,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "memchr"
version = "2.8.0"
@@ -446,6 +566,20 @@ dependencies = [
[[package]]
name = "musicfs-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"dirs",
"musicfs-cache",
"musicfs-cas",
"musicfs-core",
"musicfs-fuse",
"musicfs-metadata",
"musicfs-origins",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "musicfs-core"
@@ -508,6 +642,15 @@ version = "0.1.0"
name = "musicfs-sync"
version = "0.1.0"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -523,6 +666,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -662,6 +811,23 @@ dependencies = [
"thiserror",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rmp"
version = "0.8.15"
@@ -763,6 +929,15 @@ dependencies = [
"zmij",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -811,6 +986,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "symphonia"
version = "0.5.5"
@@ -988,6 +1169,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "tokio"
version = "1.52.3"
@@ -1045,6 +1235,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -1059,6 +1279,18 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
+3
View File
@@ -50,5 +50,8 @@ bytes = "1"
# Platform directories
dirs = "5"
# CLI
clap = { version = "4", features = ["derive"] }
# Testing
tempfile = "3"
+13
View File
@@ -8,3 +8,16 @@ name = "musicfs"
path = "src/main.rs"
[dependencies]
musicfs-core.path = "../musicfs-core"
musicfs-origins.path = "../musicfs-origins"
musicfs-cache.path = "../musicfs-cache"
musicfs-cas.path = "../musicfs-cas"
musicfs-fuse.path = "../musicfs-fuse"
musicfs-metadata.path = "../musicfs-metadata"
clap.workspace = true
tokio.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
anyhow.workspace = true
dirs.workspace = true
+194 -2
View File
@@ -1,3 +1,195 @@
fn main() {
println!("MusicFS CLI - placeholder");
use anyhow::{Context, Result};
use clap::Parser;
use musicfs_cache::TreeBuilder;
use musicfs_cas::{CasConfig, CasStore, ContentFetcher, FileReader};
use musicfs_core::{FileId, FileMeta, OriginId, RealPath, VirtualPath};
use musicfs_fuse::MusicFs;
use musicfs_metadata::MetadataParser;
use musicfs_origins::{LocalOrigin, Origin};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use tracing::{debug, info};
#[derive(Parser)]
#[command(name = "musicfs")]
#[command(about = "Virtual FUSE filesystem for music libraries")]
struct Cli {
#[arg(help = "Mount point for the virtual filesystem")]
mountpoint: PathBuf,
#[arg(short, long, help = "Source music directory (origin)")]
origin: PathBuf,
#[arg(short, long, help = "Cache directory for CAS chunks")]
cache_dir: Option<PathBuf>,
#[arg(short, long, default_value = "info", help = "Log level (debug, info, warn, error)")]
log_level: String,
}
fn main() -> Result<()> {
let cli = Cli::parse();
init_logging(&cli.log_level);
let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?;
let handle = runtime.handle().clone();
let (tree, reader) = runtime.block_on(async {
info!("MusicFS starting...");
info!("Origin: {:?}", cli.origin);
info!("Mountpoint: {:?}", cli.mountpoint);
let cache_dir = cli.cache_dir.unwrap_or_else(|| {
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("musicfs")
});
info!("Cache directory: {:?}", cache_dir);
std::fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
std::fs::create_dir_all(&cli.mountpoint).context("Failed to create mountpoint")?;
let cas_config = CasConfig {
chunks_dir: cache_dir.join("chunks"),
..Default::default()
};
let store = Arc::new(CasStore::open(cas_config).await.context("Failed to open CAS store")?);
info!("CAS store initialized");
let origin_id = OriginId::from("local");
let origin = Arc::new(LocalOrigin::new(origin_id.clone(), cli.origin.clone()));
info!("Origin registered: {}", origin.display_name());
let fetcher = Arc::new(ContentFetcher::new(store.clone()));
fetcher.register_origin(origin);
info!("Scanning music files...");
let files = scan_music_files(&cli.origin, &origin_id).await?;
info!("Found {} music files", files.len());
let mut builder = TreeBuilder::new();
for file in &files {
builder.add_file(file);
fetcher.register_file(file.clone());
}
let tree = Arc::new(RwLock::new(builder.build()));
info!("Virtual tree built");
let reader = Arc::new(FileReader::with_fetcher(store, fetcher));
Ok::<_, anyhow::Error>((tree, reader))
})?;
let fs = MusicFs::with_reader(tree, reader, handle);
info!("Mounting filesystem at {:?}", cli.mountpoint);
info!("Press Ctrl+C to unmount");
fs.mount(&cli.mountpoint).context("Failed to mount filesystem")?;
Ok(())
}
fn init_logging(level: &str) {
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(level));
tracing_subscriber::registry()
.with(fmt::layer())
.with(filter)
.init();
}
async fn scan_music_files(dir: &Path, origin_id: &OriginId) -> Result<Vec<FileMeta>> {
let parser = MetadataParser::new();
let mut files = Vec::new();
let mut file_id_counter = 1i64;
scan_dir_recursive(dir, dir, origin_id, &parser, &mut files, &mut file_id_counter).await?;
Ok(files)
}
async fn scan_dir_recursive(
base: &Path,
dir: &Path,
origin_id: &OriginId,
parser: &MetadataParser,
files: &mut Vec<FileMeta>,
id_counter: &mut i64,
) -> Result<()> {
let mut entries = tokio::fs::read_dir(dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let metadata = entry.metadata().await?;
if metadata.is_dir() {
Box::pin(scan_dir_recursive(base, &path, origin_id, parser, files, id_counter)).await?;
} else if is_audio_file(&path) {
let relative_path = path.strip_prefix(base).unwrap_or(&path);
let audio_meta = match parser.parse_file(&path) {
Ok(meta) => Some(meta),
Err(e) => {
debug!("Failed to parse metadata for {:?}: {}", path, e);
None
}
};
let virtual_path = build_virtual_path(&path, audio_meta.as_ref());
let file_meta = FileMeta {
id: FileId(*id_counter),
virtual_path,
real_path: RealPath {
origin_id: origin_id.clone(),
path: PathBuf::from("/").join(relative_path),
},
size: metadata.len(),
mtime: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
content_hash: None,
audio: audio_meta,
};
debug!("Found: {:?} -> {:?}", file_meta.real_path.path, file_meta.virtual_path);
files.push(file_meta);
*id_counter += 1;
}
}
Ok(())
}
fn is_audio_file(path: &Path) -> bool {
matches!(
path.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()).as_deref(),
Some("flac" | "mp3" | "ogg" | "wav" | "m4a" | "aac" | "opus")
)
}
fn build_virtual_path(path: &Path, audio: Option<&musicfs_core::AudioMeta>) -> VirtualPath {
if let Some(meta) = audio {
let artist = meta.artist.as_deref().unwrap_or("Unknown Artist");
let album = meta.album.as_deref().unwrap_or("Unknown Album");
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("track");
VirtualPath::new(&format!("/{}/{}/{}", sanitize(artist), sanitize(album), filename))
} else {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown");
VirtualPath::new(&format!("/Unknown Artist/Unknown Album/{}", filename))
}
}
fn sanitize(s: &str) -> String {
s.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
_ => c,
})
.collect()
}
+15 -5
View File
@@ -9,6 +9,7 @@ use std::ffi::OsStr;
use std::path::Path;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
use tokio::runtime::Handle;
use tracing::{debug, info, warn};
const TTL: Duration = Duration::from_secs(1);
@@ -17,24 +18,27 @@ const BLOCK_SIZE: u32 = 512;
pub struct MusicFs {
tree: Arc<RwLock<VirtualTree>>,
reader: Option<Arc<FileReader>>,
runtime_handle: Handle,
uid: u32,
gid: u32,
}
impl MusicFs {
pub fn new(tree: Arc<RwLock<VirtualTree>>) -> Self {
pub fn new(tree: Arc<RwLock<VirtualTree>>, runtime_handle: Handle) -> Self {
Self {
tree,
reader: None,
runtime_handle,
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
}
}
pub fn with_reader(tree: Arc<RwLock<VirtualTree>>, reader: Arc<FileReader>) -> Self {
pub fn with_reader(tree: Arc<RwLock<VirtualTree>>, reader: Arc<FileReader>, runtime_handle: Handle) -> Self {
Self {
tree,
reader: Some(reader),
runtime_handle,
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
}
@@ -241,8 +245,11 @@ impl Filesystem for MusicFs {
};
let reader = reader.clone();
let result = tokio::runtime::Handle::current().block_on(async {
reader.read(file_id, offset as u64, size).await
let handle = self.runtime_handle.clone();
let result = std::thread::scope(|_| {
handle.block_on(async {
reader.read(file_id, offset as u64, size).await
})
});
match result {
@@ -410,11 +417,14 @@ mod tests {
#[test]
fn test_tree_integration() {
let runtime = tokio::runtime::Runtime::new().unwrap();
let handle = runtime.handle().clone();
let mut builder = TreeBuilder::new();
builder.add_file(&make_file_meta(1, "/Artist/Album/Track.flac", 30_000_000));
let tree = Arc::new(RwLock::new(builder.build()));
let _fs = MusicFs::new(tree.clone());
let _fs = MusicFs::new(tree.clone(), handle);
let tree_read = tree.read().unwrap();
assert!(tree_read.get(ROOT_INODE).is_some());
+2 -4
View File
@@ -18,7 +18,7 @@
};
in
{
devShells.default = pkgs.mkShell {
devShells.default = pkgs.mkShell rec {
buildInputs = with pkgs; [
rustToolchain
pkg-config
@@ -26,20 +26,18 @@
sqlite
openssl
# Linker toolchain
clang
lld
# Dev tools
cargo-watch
cargo-nextest
cargo-criterion
# gRPC tooling (Week 10+)
protobuf
grpcurl
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
RUST_BACKTRACE = "1";
RUST_LOG = "debug";
};