Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I28cf5b7b7cff8e90e123d624d97cf9656a6a6964
61 lines
2 KiB
Rust
61 lines
2 KiB
Rust
use axum::{
|
|
extract::State,
|
|
http::header::{CONTENT_DISPOSITION, CONTENT_TYPE},
|
|
response::{IntoResponse, Response},
|
|
};
|
|
|
|
use crate::{error::ApiError, state::AppState};
|
|
|
|
/// Create a database backup and return it as a downloadable file.
|
|
/// POST /api/v1/admin/backup
|
|
///
|
|
/// For `SQLite`: creates a backup via VACUUM INTO and returns the file.
|
|
/// For `PostgreSQL`: returns unsupported error (use `pg_dump` instead).
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/v1/admin/backup",
|
|
tag = "backup",
|
|
responses(
|
|
(status = 200, description = "Backup file download"),
|
|
(status = 401, description = "Unauthorized"),
|
|
(status = 403, description = "Forbidden"),
|
|
(status = 500, description = "Internal server error"),
|
|
),
|
|
security(("bearer_auth" = []))
|
|
)]
|
|
pub async fn create_backup(
|
|
State(state): State<AppState>,
|
|
) -> Result<Response, ApiError> {
|
|
// Use a unique temp directory to avoid predictable paths
|
|
let backup_dir = std::env::temp_dir()
|
|
.join(format!("pinakes-backup-{}", uuid::Uuid::now_v7()));
|
|
tokio::fs::create_dir_all(&backup_dir)
|
|
.await
|
|
.map_err(|e| ApiError(pinakes_core::error::PinakesError::Io(e)))?;
|
|
|
|
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
|
let filename = format!("pinakes_backup_{timestamp}.db");
|
|
let backup_path = backup_dir.join(&filename);
|
|
|
|
state.storage.backup(&backup_path).await?;
|
|
|
|
// Read the backup into memory and clean up the temp file
|
|
let bytes = tokio::fs::read(&backup_path)
|
|
.await
|
|
.map_err(|e| ApiError(pinakes_core::error::PinakesError::Io(e)))?;
|
|
if let Err(e) = tokio::fs::remove_dir_all(&backup_dir).await {
|
|
tracing::warn!(path = %backup_dir.display(), error = %e, "failed to clean up backup temp dir");
|
|
}
|
|
|
|
let disposition = format!("attachment; filename=\"{filename}\"");
|
|
Ok(
|
|
(
|
|
[
|
|
(CONTENT_TYPE, "application/octet-stream".to_owned()),
|
|
(CONTENT_DISPOSITION, disposition),
|
|
],
|
|
bytes,
|
|
)
|
|
.into_response(),
|
|
)
|
|
}
|