//! Backup metadata, creation, verification, and restore HTTP handlers. use crate::auth::middleware::AuthenticatedAdmin; use crate::backup::BackupService; use crate::error::{ApiError, ApiResult}; use crate::routes::auth::GenericSuccess; use crate::state::AppState; use axum::body::Body; use axum::extract::{Path, State}; use axum::http::HeaderMap; use axum::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; use axum::response::{IntoResponse, Response}; use axum::{Extension, Json}; use chrono::Utc; use nx9_wg_core::types::backup::BackupMeta; use serde::Deserialize; use std::path::PathBuf; use uuid::Uuid; #[derive(Debug, Deserialize)] pub struct CreateBackupRecordRequest { pub filename: String, pub size_bytes: i64, pub checksum: String, pub schema_version: String, pub encrypted: Option, pub description: Option, } #[derive(Debug, Deserialize)] pub struct TriggerBackupRequest { pub description: Option, } /// GET /api/v1/backups pub async fn list_backups_handler( State(state): State, ) -> ApiResult>> { let list = state.store.list_backups().await?; Ok(Json(list)) } /// POST /api/v1/backups pub async fn create_backup_record_handler( State(state): State, Json(payload): Json, ) -> ApiResult> { let now = Utc::now().naive_utc(); let meta = BackupMeta { id: Uuid::new_v4(), filename: payload.filename, size_bytes: payload.size_bytes, checksum: payload.checksum, schema_version: payload.schema_version, encrypted: payload.encrypted.unwrap_or(false), description: payload.description, created_at: now, }; state.store.create_backup_meta(&meta).await?; Ok(Json(meta)) } /// POST /api/v1/backups/create pub async fn trigger_backup_handler( State(state): State, Extension(admin): Extension, Json(payload): Json, ) -> ApiResult> { let backup_dir = PathBuf::from("backups"); let (meta, _path) = BackupService::create_backup( &state.store, &backup_dir, payload.description.as_deref(), &admin.username, None, ) .await?; Ok(Json(meta)) } /// GET /api/v1/backups/{id} pub async fn get_backup_handler( State(state): State, Path(id): Path, ) -> ApiResult> { let meta = state .store .get_backup_meta(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Backup record '{id}' not found")))?; Ok(Json(meta)) } /// GET /api/v1/backups/{id}/download pub async fn download_backup_handler( State(state): State, Path(id): Path, ) -> ApiResult { let meta = state .store .get_backup_meta(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Backup record '{id}' not found")))?; let backup_dir = PathBuf::from("backups"); let file_path = backup_dir.join(&meta.filename); if !file_path.exists() { return Err(ApiError::NotFound(format!( "Backup archive file '{}' not found on disk", meta.filename ))); } let bytes = std::fs::read(&file_path) .map_err(|e| ApiError::Internal(format!("Failed to read backup file for download: {e}")))?; let mut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, "application/octet-stream".parse().unwrap()); headers.insert( CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", meta.filename) .parse() .unwrap(), ); Ok((headers, Body::from(bytes)).into_response()) } /// DELETE /api/v1/backups/{id} pub async fn delete_backup_handler( State(state): State, Path(id): Path, ) -> ApiResult> { let meta = state.store.get_backup_meta(id).await?; if let Some(m) = meta { let backup_dir = PathBuf::from("backups"); let file_path = backup_dir.join(&m.filename); let _ = std::fs::remove_file(file_path); } state.store.delete_backup_meta(id).await?; Ok(Json(GenericSuccess { success: true, message: format!("Backup record '{id}' deleted"), })) } /// POST /api/v1/backups/{id}/restore pub async fn restore_backup_handler( State(state): State, Extension(admin): Extension, Path(id): Path, ) -> ApiResult> { let meta = state .store .get_backup_meta(id) .await? .ok_or_else(|| ApiError::NotFound(format!("Backup record '{id}' not found")))?; let backup_dir = PathBuf::from("backups"); let file_path = backup_dir.join(&meta.filename); let active_db = PathBuf::from("nx9-wg.db"); let safety_dir = backup_dir.join("safety"); BackupService::restore_backup( &state.store, &file_path, &active_db, &safety_dir, &admin.username, None, ) .await?; Ok(Json(GenericSuccess { success: true, message: format!( "Database successfully restored from backup '{}'", meta.filename ), })) }