Files
PostfixAdminRS/src/routes/backup.rs
T
2026-11-03 16:52:28 +01:00

86 lines
2.8 KiB
Rust

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<Arc<AppState>>, jar: CookieJar) -> AppResult<Response> {
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<u8>, name: &str) -> AppResult<Response> {
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;