Files
nx9-wg/crates/nx9-wg-api/src/routes/backups.rs
T
thakaresandCopilot 2ac6c81dfe cli: avoid data-dir initialization for version; create db parent dirs; redact generated passwords in CLI output
- Prevent 'nx9-wg version' from creating data directories by avoiding database initialization.
- Create parent directories when an explicit --database path is provided.
- Redact printed generated administrator passwords; announce file path or redact instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-16 16:26:24 +05:30

186 lines
5.2 KiB
Rust

//! 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<bool>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TriggerBackupRequest {
pub description: Option<String>,
}
/// GET /api/v1/backups
pub async fn list_backups_handler(
State(state): State<AppState>,
) -> ApiResult<Json<Vec<BackupMeta>>> {
let list = state.store.list_backups().await?;
Ok(Json(list))
}
/// POST /api/v1/backups
pub async fn create_backup_record_handler(
State(state): State<AppState>,
Json(payload): Json<CreateBackupRecordRequest>,
) -> ApiResult<Json<BackupMeta>> {
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<AppState>,
Extension(admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<TriggerBackupRequest>,
) -> ApiResult<Json<BackupMeta>> {
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<AppState>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<BackupMeta>> {
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<AppState>,
Path(id): Path<Uuid>,
) -> ApiResult<Response> {
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<AppState>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<GenericSuccess>> {
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<AppState>,
Extension(admin): Extension<AuthenticatedAdmin>,
Path(id): Path<Uuid>,
) -> ApiResult<Json<GenericSuccess>> {
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
),
}))
}