Files
MusicFS/crates/musicfs-client/src/main.rs
T
2026-07-01 16:56:56 +02:00

170 lines
6.1 KiB
Rust

use std::{collections::HashMap, net::SocketAddr, path::Path, sync::Arc};
use clap::Parser;
use fuser::INodeNo;
use musicfs::db::entities::{Entity, Model};
use musicfs::db::sync::sync_items_to_db;
use musicfs::health::ServerStatus;
use musicfs::item::Item;
use musicfs::logging::{LogConfig, init};
use musicfs::music::db::restore_music_metadata_from_db;
use musicfs::origins::local::LocalOrigin;
use musicfs::origins::network::NetworkOrigin;
use musicfs::origins::{FuseFs, Origin};
use musicfs::virtual_dirs::restore_virtual_paths;
use sea_orm::entity::prelude::*;
use tokio::signal::unix::{SignalKind, signal};
use tracing::{debug, error, info};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short, long, required = true)]
mountpoint: String,
/// Local directory path (→ LocalOrigin) OR `http://host:port` URL of a
/// musicfs-server (→ NetworkOrigin).
#[arg(short, long, required = true)]
source: String,
#[arg(short, long, required = true)]
database: String,
/// Directory for daily-rotated log files.
#[arg(long, default_value = "./logs")]
log_dir: std::path::PathBuf,
/// gRPC server address for health, status, and control RPCs.
#[arg(long, default_value = "127.0.0.1:50052")]
listen: SocketAddr,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
// Bind the guard for the whole process so the non-blocking file writer
// flushes on exit. Initialized before anything else so even early
// failures land in the log.
let _guard = init(LogConfig {
log_dir: args.log_dir.clone(),
file_prefix: "musicfs".to_string(),
max_files: 7,
});
let mountpoint = args.mountpoint.clone();
info!(
mountpoint = %mountpoint,
source = %args.source,
database = %args.database,
log_dir = %args.log_dir.display(),
"musicfs starting"
);
// sea-orm logs every statement at INFO by default, which floods normal
// output; demote to DEBUG so queries stay hidden under RUST_LOG=info.
let mut db_opts = sea_orm::ConnectOptions::new(args.database.clone());
db_opts.sqlx_logging_level(log::LevelFilter::Debug);
let db = sea_orm::Database::connect(db_opts)
.await
.unwrap_or_else(|e| {
error!(database = %args.database, error = %e, "database connect failed");
panic!("database connect: {e}");
});
info!(database = %args.database, "database connected");
let (snapshot, byte_source, watcher, server_status) = if looks_like_url(&args.source) {
debug!(source = %args.source, "using NetworkOrigin");
let origin = NetworkOrigin::new(args.source.clone(), mountpoint.clone(), db.clone())
.unwrap_or_else(|e| {
error!(source = %args.source, error = %e, "network origin init failed");
panic!("network origin init: {e}");
});
let server_status = origin.server_status();
let snapshot = origin.snapshot_async().await.unwrap_or_else(|e| {
error!(source = %args.source, error = %e, "network initial snapshot failed");
panic!("network initial snapshot: {e}");
});
info!(files = snapshot.len(), "network snapshot complete");
(
snapshot,
origin.byte_source(),
origin.watcher(),
server_status,
)
} else {
debug!(source = %args.source, "using LocalOrigin");
let origin = LocalOrigin::new(args.source.clone(), mountpoint.clone());
let mut snapshot = origin.snapshot().unwrap_or_else(|e| {
error!(source = %args.source, error = %e, "local initial snapshot failed");
panic!("local initial snapshot: {e}");
});
let db_items: HashMap<i64, Model> = Entity::find()
.all(&db)
.await
.unwrap_or_else(|e| {
error!(error = %e, "loading db items failed");
panic!("loading db items: {e}");
})
.into_iter()
.map(|e| (e.inode, e))
.collect();
sync_items_to_db(&snapshot, &db_items, &db).await;
restore_music_metadata_from_db(&mut snapshot, &db_items, &db).await;
restore_virtual_paths(&mut snapshot, &db_items, Path::new(&args.source));
info!(files = snapshot.len(), "local snapshot complete");
let server_status = ServerStatus::new_local(mountpoint.clone());
(
snapshot,
origin.byte_source(),
origin.watcher(),
server_status,
)
};
let files: Arc<std::sync::Mutex<std::collections::BTreeMap<INodeNo, Item>>> =
Arc::new(std::sync::Mutex::new(snapshot));
let watcher_handle = watcher.watch(files.clone());
let health_files = files.clone();
let fs = FuseFs {
files,
bytes: byte_source,
client: db,
runtime_handle: tokio::runtime::Handle::current(),
};
let cfg = fuser::Config::default();
let session = fuser::spawn_mount2(fs, &mountpoint, &cfg).unwrap_or_else(|e| {
error!(mountpoint = %mountpoint, error = %e, "failed to mount FUSE filesystem");
panic!("failed to mount FUSE filesystem: {e}");
});
info!(mountpoint = %mountpoint, "FUSE mounted");
musicfs::health::spawn_health_server(args.listen, server_status, health_files).await;
let mut sigint = signal(SignalKind::interrupt()).unwrap_or_else(|e| {
error!(error = %e, "failed to register SIGINT handler");
panic!("register SIGINT handler: {e}");
});
let mut sigterm = signal(SignalKind::terminate()).unwrap_or_else(|e| {
error!(error = %e, "failed to register SIGTERM handler");
panic!("register SIGTERM handler: {e}");
});
tokio::select! {
_ = sigint.recv() => info!("received SIGINT, shutting down"),
_ = sigterm.recv() => info!("received SIGTERM, shutting down"),
}
info!(mountpoint = %mountpoint, "unmounting");
watcher_handle.stop();
drop(session);
}
fn looks_like_url(s: &str) -> bool {
return s.starts_with("http://") || s.starts_with("https://");
}