From 712cf1deb921736d25663739a3f5031833fa7104 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 16 Mar 2026 03:58:36 +0100 Subject: [PATCH] fix: local_files hot-reload via RwLock state fields + rebuild_registry local_files case --- k-tv-backend/api/src/main.rs | 25 ++++++-- .../api/src/routes/admin_providers.rs | 59 +++++++++++++++++++ k-tv-backend/api/src/routes/files.rs | 35 ++++------- k-tv-backend/api/src/state.rs | 17 ++++-- 4 files changed, 102 insertions(+), 34 deletions(-) diff --git a/k-tv-backend/api/src/main.rs b/k-tv-backend/api/src/main.rs index 135caba..d487dd1 100644 --- a/k-tv-backend/api/src/main.rs +++ b/k-tv-backend/api/src/main.rs @@ -80,6 +80,14 @@ async fn main() -> anyhow::Result<()> { let db_pool = k_core::db::connect(&db_config).await?; run_migrations(&db_pool).await?; + #[cfg(feature = "local-files")] + let raw_sqlite_pool: Option = match &db_pool { + #[cfg(feature = "sqlite")] + k_core::db::DatabasePool::Sqlite(p) => Some(p.clone()), + #[allow(unreachable_patterns)] + _ => None, + }; + let user_repo = build_user_repository(&db_pool).await?; let channel_repo = build_channel_repository(&db_pool).await?; let schedule_repo = build_schedule_repository(&db_pool).await?; @@ -220,10 +228,19 @@ async fn main() -> anyhow::Result<()> { .await?; #[cfg(feature = "local-files")] - { - state.local_index = local_index; - state.transcode_manager = transcode_manager; - state.sqlite_pool = sqlite_pool_for_state; + { state.raw_sqlite_pool = raw_sqlite_pool; } + + #[cfg(feature = "local-files")] + if let Some(idx) = local_index { + *state.local_index.write().await = Some(idx); + } + #[cfg(feature = "local-files")] + if let Some(tm) = transcode_manager { + *state.transcode_manager.write().await = Some(tm); + } + #[cfg(feature = "local-files")] + if let Some(pool) = sqlite_pool_for_state { + *state.sqlite_pool.write().await = Some(pool); } let server_config = ServerConfig { diff --git a/k-tv-backend/api/src/routes/admin_providers.rs b/k-tv-backend/api/src/routes/admin_providers.rs index a2341ac..16809c3 100644 --- a/k-tv-backend/api/src/routes/admin_providers.rs +++ b/k-tv-backend/api/src/routes/admin_providers.rs @@ -113,6 +113,65 @@ async fn rebuild_registry(state: &AppState) -> DomainResult<()> { ); } } + #[cfg(feature = "local-files")] + "local_files" => { + let config: std::collections::HashMap = + match serde_json::from_str(&row.config_json) { + Ok(c) => c, + Err(_) => continue, + }; + + let files_dir = match config.get("files_dir") { + Some(d) => std::path::PathBuf::from(d), + None => continue, + }; + + let transcode_dir = config + .get("transcode_dir") + .filter(|s| !s.is_empty()) + .map(std::path::PathBuf::from); + + let cleanup_ttl_hours: u32 = config + .get("cleanup_ttl_hours") + .and_then(|s| s.parse().ok()) + .unwrap_or(24); + + let base_url = state.config.base_url.clone(); + + let sqlite_pool = match &state.raw_sqlite_pool { + Some(p) => p.clone(), + None => { + tracing::warn!("local_files provider requires SQLite; skipping"); + continue; + } + }; + + let lf_cfg = infra::LocalFilesConfig { + root_dir: files_dir, + base_url, + transcode_dir: transcode_dir.clone(), + cleanup_ttl_hours, + }; + + let idx = Arc::new(infra::LocalIndex::new(&lf_cfg, sqlite_pool.clone()).await); + + let scan_idx = Arc::clone(&idx); + tokio::spawn(async move { scan_idx.rescan().await; }); + + let tm = transcode_dir.as_ref().map(|td| { + std::fs::create_dir_all(td).ok(); + infra::TranscodeManager::new(td.clone(), cleanup_ttl_hours) + }); + + new_registry.register( + "local", + Arc::new(infra::LocalFilesProvider::new(Arc::clone(&idx), lf_cfg, tm.clone())), + ); + + *state.local_index.write().await = Some(idx); + *state.transcode_manager.write().await = tm; + *state.sqlite_pool.write().await = Some(sqlite_pool); + } _ => {} } } diff --git a/k-tv-backend/api/src/routes/files.rs b/k-tv-backend/api/src/routes/files.rs index 90eaf64..11b742e 100644 --- a/k-tv-backend/api/src/routes/files.rs +++ b/k-tv-backend/api/src/routes/files.rs @@ -147,9 +147,7 @@ async fn trigger_rescan( State(state): State, CurrentUser(_user): CurrentUser, ) -> Result, ApiError> { - let index = state - .local_index - .as_ref() + let index = state.local_index.read().await.clone() .ok_or_else(|| ApiError::not_implemented("no local files provider active"))?; let count = index.rescan().await; Ok(Json(serde_json::json!({ "items_found": count }))) @@ -164,9 +162,7 @@ async fn transcode_playlist( State(state): State, Path(id): Path, ) -> Result { - let tm = state - .transcode_manager - .as_ref() + let tm = state.transcode_manager.read().await.clone() .ok_or_else(|| ApiError::not_implemented("TRANSCODE_DIR not configured"))?; let root = state.config.local_files_dir.as_ref().ok_or_else(|| { @@ -219,9 +215,7 @@ async fn transcode_segment( return Err(ApiError::Forbidden("invalid segment path".into())); } - let tm = state - .transcode_manager - .as_ref() + let tm = state.transcode_manager.read().await.clone() .ok_or_else(|| ApiError::not_implemented("TRANSCODE_DIR not configured"))?; let file_path = tm.transcode_dir.join(&id).join(&segment); @@ -262,14 +256,12 @@ async fn get_transcode_settings( State(state): State, CurrentUser(_user): CurrentUser, ) -> Result, ApiError> { - let pool = state - .sqlite_pool - .as_ref() + let pool = state.sqlite_pool.read().await.clone() .ok_or_else(|| ApiError::not_implemented("sqlite not available"))?; let (ttl,): (i64,) = sqlx::query_as("SELECT cleanup_ttl_hours FROM transcode_settings WHERE id = 1") - .fetch_one(pool) + .fetch_one(&pool) .await .map_err(|e| ApiError::internal(e.to_string()))?; @@ -284,19 +276,18 @@ async fn update_transcode_settings( CurrentUser(_user): CurrentUser, Json(req): Json, ) -> Result, ApiError> { - let pool = state - .sqlite_pool - .as_ref() + let pool = state.sqlite_pool.read().await.clone() .ok_or_else(|| ApiError::not_implemented("sqlite not available"))?; let ttl = req.cleanup_ttl_hours as i64; sqlx::query("UPDATE transcode_settings SET cleanup_ttl_hours = ? WHERE id = 1") .bind(ttl) - .execute(pool) + .execute(&pool) .await .map_err(|e| ApiError::internal(e.to_string()))?; - if let Some(tm) = &state.transcode_manager { + let tm_opt = state.transcode_manager.read().await.clone(); + if let Some(tm) = tm_opt { tm.set_cleanup_ttl(req.cleanup_ttl_hours); } @@ -310,9 +301,7 @@ async fn get_transcode_stats( State(state): State, CurrentUser(_user): CurrentUser, ) -> Result, ApiError> { - let tm = state - .transcode_manager - .as_ref() + let tm = state.transcode_manager.read().await.clone() .ok_or_else(|| ApiError::not_implemented("TRANSCODE_DIR not configured"))?; let (cache_size_bytes, item_count) = tm.cache_stats().await; Ok(Json(TranscodeStatsResponse { @@ -326,9 +315,7 @@ async fn clear_transcode_cache( State(state): State, CurrentUser(_user): CurrentUser, ) -> Result { - let tm = state - .transcode_manager - .as_ref() + let tm = state.transcode_manager.read().await.clone() .ok_or_else(|| ApiError::not_implemented("TRANSCODE_DIR not configured"))?; tm.clear_cache() .await diff --git a/k-tv-backend/api/src/state.rs b/k-tv-backend/api/src/state.rs index a7d7e18..a7c3f79 100644 --- a/k-tv-backend/api/src/state.rs +++ b/k-tv-backend/api/src/state.rs @@ -39,13 +39,16 @@ pub struct AppState { pub activity_log_repo: Arc, /// Index for the local-files provider, used by the rescan route. #[cfg(feature = "local-files")] - pub local_index: Option>, + pub local_index: Arc>>>, /// TranscodeManager for FFmpeg HLS transcoding (requires TRANSCODE_DIR). #[cfg(feature = "local-files")] - pub transcode_manager: Option>, + pub transcode_manager: Arc>>>, /// SQLite pool for transcode settings CRUD. #[cfg(feature = "local-files")] - pub sqlite_pool: Option, + pub sqlite_pool: Arc>>, + /// Raw sqlite pool — always present when running SQLite, used for local-files hot-reload. + #[cfg(feature = "local-files")] + pub raw_sqlite_pool: Option, } impl AppState { @@ -137,11 +140,13 @@ impl AppState { log_history, activity_log_repo, #[cfg(feature = "local-files")] - local_index: None, + local_index: Arc::new(tokio::sync::RwLock::new(None)), #[cfg(feature = "local-files")] - transcode_manager: None, + transcode_manager: Arc::new(tokio::sync::RwLock::new(None)), #[cfg(feature = "local-files")] - sqlite_pool: None, + sqlite_pool: Arc::new(tokio::sync::RwLock::new(None)), + #[cfg(feature = "local-files")] + raw_sqlite_pool: None, }) } }