use std::path::PathBuf;
use std::sync::Arc;
use arc_swap::ArcSwap;
use tracing::info;
use crate::nzb_core::config::AppConfig;
use crate::nzb_core::db::Database;
use crate::auth::{CredentialStore, TokenStore};
use crate::log_buffer::LogBuffer;
use crate::queue_manager::QueueManager;
use crate::state::AppState;
fn sanitize_loaded_config(config: &mut AppConfig) {
for server in &mut config.servers {
let trim = |value: &mut String| {
let trimmed = value.trim();
if trimmed.len() != value.len() {
*value = trimmed.to_string();
}
};
let trim_opt = |value: &mut Option<String>| {
if let Some(inner) = value.as_mut() {
trim(inner);
}
};
trim(&mut server.host);
trim(&mut server.name);
trim_opt(&mut server.username);
trim_opt(&mut server.password);
trim_opt(&mut server.proxy_url);
trim_opt(&mut server.trusted_fingerprint);
}
}
pub struct StartupConfig {
pub config_path: PathBuf,
pub listen_addr: Option<String>,
pub port: Option<u16>,
pub data_dir: Option<PathBuf>,
pub log_level: Option<String>,
}
pub struct StartupResult {
pub state: Arc<AppState>,
pub queue_manager: Arc<QueueManager>,
pub log_buffer: LogBuffer,
}
pub async fn initialize(
startup: StartupConfig,
log_buffer: Option<LogBuffer>,
) -> anyhow::Result<StartupResult> {
let config_path = startup.config_path;
let mut config = AppConfig::load(&config_path)?;
sanitize_loaded_config(&mut config);
if let Some(addr) = startup.listen_addr {
config.general.listen_addr = addr;
}
if let Some(port) = startup.port {
config.general.port = port;
}
if let Some(data_dir) = startup.data_dir {
config.general.data_dir = data_dir;
}
if let Ok(val) = std::env::var("OTEL_ENABLED") {
config.otel.enabled = val == "true" || val == "1";
}
if let Ok(val) = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
config.otel.endpoint = val;
}
if let Ok(val) = std::env::var("OTEL_SERVICE_NAME") {
config.otel.service_name = val;
}
std::fs::create_dir_all(&config.general.data_dir).map_err(|e| {
anyhow::anyhow!(
"Cannot create data dir '{}': {} — check PUID/PGID settings and volume mount permissions",
config.general.data_dir.display(), e
)
})?;
std::fs::create_dir_all(&config.general.incomplete_dir).map_err(|e| {
anyhow::anyhow!(
"Cannot create incomplete dir '{}': {} — check PUID/PGID settings and volume mount permissions",
config.general.incomplete_dir.display(), e
)
})?;
std::fs::create_dir_all(&config.general.complete_dir).map_err(|e| {
anyhow::anyhow!(
"Cannot create complete dir '{}': {} — check PUID/PGID settings and volume mount permissions",
config.general.complete_dir.display(), e
)
})?;
let db_path = config.general.data_dir.join("rustnzb.db");
let db = Database::open(&db_path)?;
info!(path = %db_path.display(), "Database opened");
let log_buffer = log_buffer.unwrap_or_default();
let queue_manager = QueueManager::new(
config.servers.clone(),
db,
config.general.incomplete_dir.clone(),
config.general.complete_dir.clone(),
log_buffer.clone(),
config.general.max_active_downloads,
config.categories.clone(),
config.general.min_free_space_bytes,
config.general.speed_limit_bps,
config.general.direct_unpack,
config.general.abort_hopeless,
config.general.early_failure_check,
config.general.required_completion_pct,
config.general.article_timeout_secs,
Some(crate::ServerProbePolicy::default()),
);
if let Some(retention) = config.general.history_retention {
queue_manager.set_history_retention(Some(retention));
}
if let Err(e) = queue_manager.restore_from_db() {
tracing::warn!("Failed to restore queue from database: {e}");
}
queue_manager.spawn_speed_tracker();
info!(servers = config.servers.len(), "Queue manager initialized");
if let Some(ref watch_dir) = config.general.watch_dir {
let watcher =
crate::dir_watcher::DirWatcher::new(watch_dir.clone(), Arc::clone(&queue_manager));
tokio::spawn(async move { watcher.run().await });
info!(dir = %watch_dir.display(), "Directory watcher started");
}
let credential_store = Arc::new(CredentialStore::new(config.general.data_dir.clone()));
let token_store = Arc::new(TokenStore::new());
if credential_store.has_credentials() {
info!("Authentication enabled (credentials configured)");
} else {
info!("Authentication not yet configured; first-boot setup required");
}
let shared_config = Arc::new(ArcSwap::new(Arc::new(config)));
let data_dir_for_rss = shared_config.load().general.data_dir.clone();
let monitor = crate::rss_monitor::RssMonitor::new(
Arc::clone(&shared_config),
Arc::clone(&queue_manager),
data_dir_for_rss,
);
tokio::spawn(async move { monitor.run().await });
let state = Arc::new(AppState::new(
shared_config,
config_path,
Arc::clone(&queue_manager),
log_buffer.clone(),
token_store,
credential_store,
));
Ok(StartupResult {
state,
queue_manager,
log_buffer,
})
}
#[cfg(test)]
mod tests {
use super::sanitize_loaded_config;
use crate::nzb_core::config::AppConfig;
use crate::nzb_core::config::ServerConfig;
#[test]
fn sanitize_loaded_config_trims_server_fields() {
let mut config = AppConfig::default();
let mut server = ServerConfig::new("srv-1", " news.example.com \n");
server.name = " Primary ".into();
server.username = Some(" user ".into());
server.password = Some(" pass ".into());
server.proxy_url = Some(" socks5://proxy ".into());
server.trusted_fingerprint = Some(" abc123 ".into());
config.servers.push(server);
sanitize_loaded_config(&mut config);
let server = &config.servers[0];
assert_eq!(server.host, "news.example.com");
assert_eq!(server.name, "Primary");
assert_eq!(server.username.as_deref(), Some("user"));
assert_eq!(server.password.as_deref(), Some("pass"));
assert_eq!(server.proxy_url.as_deref(), Some("socks5://proxy"));
assert_eq!(server.trusted_fingerprint.as_deref(), Some("abc123"));
}
}