170 lines
6.1 KiB
Rust
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://");
|
|
}
|