diff --git a/Cargo.lock b/Cargo.lock index 878d214..05dccf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1940,6 +1940,7 @@ dependencies = [ "sd-notify", "tokio", "tokio-util 0.7.18", + "toml", "tracing", "tracing-appender", "tracing-journald", diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..1b9ad89 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,25 @@ +mount_point = "/mnt/music" +cache_dir = "/var/cache/musicfs" + +[[origins]] +id = "local-storage" +origin_type = "local" +priority = 1 +enabled = true +path = "/path/to/local/music" + +[cache] +metadata_cache_mb = 100 +content_cache_gb = 10 + +[health] +check_interval_secs = 30 +timeout_ms = 5000 +unhealthy_threshold = 3 + +[logging] +log_dir = "/var/log/musicfs" +json_output = false +journald = true +level = "musicfs=info,warn" +trace_sample_rate = 1.0 diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..b6d99d5 --- /dev/null +++ b/config.toml @@ -0,0 +1,25 @@ +mount_point = "./dev/music" +cache_dir = "./dev/cache/musicfs" + +[[origins]] +id = "local-storage" +origin_type = "local" +priority = 1 +enabled = true +path = "/home/fujin/.local/share/docker/volumes/containers_downloads/_data" + +[cache] +metadata_cache_mb = 100 +content_cache_gb = 10 + +[health] +check_interval_secs = 30 +timeout_ms = 5000 +unhealthy_threshold = 3 + +[logging] +log_dir = "./dev/log" +json_output = false +journald = true +level = "musicfs=info,warn" +trace_sample_rate = 1.0 diff --git a/crates/musicfs-cli/Cargo.toml b/crates/musicfs-cli/Cargo.toml index 6401a70..a35dfe8 100644 --- a/crates/musicfs-cli/Cargo.toml +++ b/crates/musicfs-cli/Cargo.toml @@ -23,6 +23,7 @@ tracing-subscriber.workspace = true tracing-appender.workspace = true anyhow.workspace = true dirs.workspace = true +toml.workspace = true parking_lot.workspace = true libc.workspace = true diff --git a/crates/musicfs-cli/src/main.rs b/crates/musicfs-cli/src/main.rs index d7d327f..6452ee9 100644 --- a/crates/musicfs-cli/src/main.rs +++ b/crates/musicfs-cli/src/main.rs @@ -7,12 +7,14 @@ use musicfs_fuse::MusicFs; use musicfs_metadata::MetadataParser; use musicfs_origins::{LocalOrigin, Origin}; use parking_lot::RwLock; +use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::SystemTime; +use toml::Value; use tracing::{debug, info, warn}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{fmt, prelude::*, EnvFilter, Layer}; @@ -115,17 +117,54 @@ fn main() -> Result<()> { match cli.command { Commands::Mount { - config: _, + config, mountpoint, origin, cache_dir, } => { - let log_config = LoggingConfig { - level: cli.log_level, - ..Default::default() + let mut config = if let Some(config_path) = config { + musicfs_core::Config::from_file(&config_path)? + } else { + let origin_path = origin + .context("--origin is required for mount if no config file is provided")?; + let cache_dir = cache_dir.clone().unwrap_or_else(|| { + dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from("/tmp")) + .join("musicfs") + }); + + let mut settings = HashMap::new(); + settings.insert( + "path".to_string(), + Value::String(origin_path.to_string_lossy().into_owned()), + ); + + musicfs_core::Config { + mount_point: mountpoint.clone(), + cache_dir: cache_dir.clone(), + origins: vec![musicfs_core::OriginConfig { + id: "local".to_string(), + origin_type: musicfs_core::OriginType::Local, + priority: 1, + enabled: true, + settings, + }], + cache: Default::default(), + health: Default::default(), + logging: LoggingConfig { + level: cli.log_level.clone(), + ..Default::default() + }, + } }; - let _guard = init_logging(&log_config)?; - run_mount(mountpoint, origin, cache_dir) + + if let Some(c_dir) = cache_dir { + config.cache_dir = c_dir; + } + config.mount_point = mountpoint; + + let _guard = init_logging(&config.logging)?; + run_mount(config) } Commands::Status => { init_basic_logging(&cli.log_level); @@ -154,32 +193,19 @@ fn main() -> Result<()> { } } -fn run_mount( - mountpoint: PathBuf, - origin_path: Option, - cache_dir: Option, -) -> Result<()> { - let origin_path = origin_path.context("--origin is required for mount")?; - - let cache_dir = cache_dir.unwrap_or_else(|| { - dirs::cache_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join("musicfs") - }); - +fn run_mount(config: musicfs_core::Config) -> Result<()> { let runtime = tokio::runtime::Runtime::new().context("Failed to create Tokio runtime")?; let handle = runtime.handle().clone(); - let cache_dir_clone = cache_dir.clone(); let (tree, reader) = runtime.block_on(async { - info!(origin = ?origin_path, mountpoint = ?mountpoint, "Mount configuration"); - info!("Cache directory: {:?}", cache_dir_clone); + info!(mountpoint = ?config.mount_point, "Mount configuration"); + info!("Cache directory: {:?}", config.cache_dir); - std::fs::create_dir_all(&cache_dir_clone).context("Failed to create cache directory")?; - std::fs::create_dir_all(&mountpoint).context("Failed to create mountpoint")?; + std::fs::create_dir_all(&config.cache_dir).context("Failed to create cache directory")?; + std::fs::create_dir_all(&config.mount_point).context("Failed to create mountpoint")?; let cas_config = CasConfig { - chunks_dir: cache_dir_clone.join("chunks"), + chunks_dir: config.cache_dir.join("chunks"), ..Default::default() }; let store = Arc::new( @@ -189,16 +215,53 @@ fn run_mount( ); info!("CAS store initialized"); - let origin_id = OriginId::from("local"); - let origin = Arc::new(LocalOrigin::new(origin_id.clone(), origin_path.clone())); - info!("Origin registered: {}", origin.display_name()); - let fetcher = Arc::new(ContentFetcher::new(store.clone())); - fetcher.register_origin(origin); + let mut files = Vec::new(); - info!("Scanning music files..."); - let files = scan_music_files(&origin_path, &origin_id).await?; - info!("Found {} music files", files.len()); + for origin_cfg in &config.origins { + if !origin_cfg.enabled { + continue; + } + + let origin_id = OriginId::from(origin_cfg.id.as_str()); + let origin: Arc = match origin_cfg.origin_type { + musicfs_core::OriginType::Local => { + let path_str = origin_cfg + .settings + .get("path") + .and_then(|v| v.as_str()) + .context("path required for local origin")?; + Arc::new(LocalOrigin::new(origin_id.clone(), PathBuf::from(path_str))) + } + _ => { + warn!( + "Origin type {:?} not supported in CLI yet, skipping", + origin_cfg.origin_type + ); + continue; + } + }; + + info!("Origin registered: {}", origin.display_name()); + fetcher.register_origin(origin.clone()); + + if origin_cfg.origin_type == musicfs_core::OriginType::Local { + let path_str = origin_cfg + .settings + .get("path") + .and_then(|v| v.as_str()) + .unwrap(); + let origin_path = PathBuf::from(path_str); + info!("Scanning music files for origin {}...", origin_cfg.id); + let origin_files = scan_music_files(&origin_path, &origin_id).await?; + info!( + "Fount {} music files for origin {}", + origin_files.len(), + origin_cfg.id + ); + files.extend(origin_files); + } + } let mut builder = TreeBuilder::new(); for file in &files { @@ -213,19 +276,19 @@ fn run_mount( Ok::<_, anyhow::Error>((tree, reader)) })?; - check_stale_mount(&mountpoint)?; + check_stale_mount(&config.mount_point)?; - let lock_path = cache_dir.join("musicfs.lock"); + let lock_path = config.cache_dir.join("musicfs.lock"); let _lock = try_acquire_lock(&lock_path) .context("Failed to acquire lock — is another instance running?")?; info!(lock_path = ?lock_path, "Lock acquired"); let fs = MusicFs::with_reader(tree, reader, handle.clone()); - info!("Mounting filesystem at {:?}", mountpoint); + info!("Mounting filesystem at {:?}", config.mount_point); let session = fs - .spawn_mount(&mountpoint) + .spawn_mount(&config.mount_point) .context("Failed to mount filesystem")?; #[cfg(target_os = "linux")] diff --git a/flake.nix b/flake.nix index 2dd29f8..ff3e2a7 100644 --- a/flake.nix +++ b/flake.nix @@ -63,6 +63,7 @@ clang lld + crates-lsp protobuf grpcurl