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, ) -> Result { // 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(), ) }