- 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>
186 lines
5.2 KiB
Rust
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
|
|
),
|
|
}))
|
|
}
|