use crate::{ db::Dialect, error::{AppError, AppResult}, routes, state::AppState, }; use axum::{ body::Body, extract::State, http::{HeaderMap, HeaderValue, header}, response::Response, }; use axum_extra::extract::cookie::CookieJar; use std::{process::Stdio, sync::Arc}; use tokio::process::Command; pub async fn download(State(state): State>, jar: CookieJar) -> AppResult { let s = routes::session(&state, &jar)?; s.require_global()?; if !state.config.features.backup { return Err(AppError::forbidden("Backup is disabled")); } if state.db.dialect() == Dialect::Sqlite { let path = state .config .database .path .as_deref() .unwrap_or(&state.config.database.name); let bytes = tokio::fs::read(path).await?; return response(bytes, "postfixadmin.sqlite"); } let mut cmd = if state.db.dialect() == Dialect::Postgres { let mut c = Command::new(&state.config.backup.pg_dump); c.arg("--no-owner") .arg("--no-privileges") .arg("--host") .arg(&state.config.database.host) .arg("--port") .arg(state.config.database.port.to_string()) .arg("--username") .arg(&state.config.database.user) .arg(&state.config.database.name); c.env("PGPASSWORD", &state.config.database.password); c } else { let mut c = Command::new(&state.config.backup.mysql_dump); c.arg("--single-transaction") .arg("--skip-lock-tables") .arg("--host") .arg(&state.config.database.host) .arg("--port") .arg(state.config.database.port.to_string()) .arg("--user") .arg(&state.config.database.user) .arg(&state.config.database.name); c.env("MYSQL_PWD", &state.config.database.password); c }; let out = cmd .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await?; if !out.status.success() { tracing::error!(stderr=%String::from_utf8_lossy(&out.stderr),"database dump failed"); return Err(AppError::config("Database dump command failed")); } response(out.stdout, "postfixadmin.sql") } fn response(bytes: Vec, name: &str) -> AppResult { let mut headers = HeaderMap::new(); headers.insert( header::CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"), ); headers.insert( header::CONTENT_DISPOSITION, HeaderValue::from_str(&format!("attachment; filename=\"{name}\"")) .map_err(|_| AppError::config("invalid backup filename"))?, ); Ok((headers, Body::from(bytes)).into_response()) } use axum::response::IntoResponse;