Files
nx9-url-shortener/src/web/admin/backups.rs
T
thakares f49698bb5c
Rust CI / Test & Quality Checks (push) Canceled after 0s
Rust CI / Build Docker Image (push) Canceled after 0s
Release v0.6.0
2026-08-09 17:17:57 +05:30

301 lines
9.7 KiB
Rust

use super::*;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct BackupFileRow {
pub filename: String,
pub size_str: String,
pub created_str: String,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct BackupHistoryRow {
pub id: String,
pub backup_path: String,
pub status: String,
pub created_at: String,
pub size_bytes: i64,
pub error_message: Option<String>,
}
// GET /admin/backups
pub async fn backups_get(
State(state): State<AppState>,
jar: CookieJar,
Query(params): Query<HashMap<String, String>>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let mut files = vec![];
if let Ok(dir_entries) = std::fs::read_dir(&state.config.backup_dir) {
for entry in dir_entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(filename) = path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
{
if filename.ends_with(".tar.gz") {
let meta = entry.metadata().unwrap();
let size_str = format_size(meta.len());
let created_str = meta
.created()
.ok()
.map(|c| {
let datetime: chrono::DateTime<chrono::Utc> = c.into();
datetime.format("%Y-%m-%d %H:%M:%S").to_string()
})
.unwrap_or_else(|| "-".to_string());
files.push(BackupFileRow {
filename,
size_str,
created_str,
});
}
}
}
}
}
files.sort_by(|a, b| b.filename.cmp(&a.filename));
let history = {
let conn = state.system_db.lock().unwrap();
let mut stmt = conn.prepare("SELECT id, backup_path, status, created_at, size_bytes, error_message FROM backup_history ORDER BY created_at DESC LIMIT 30;").unwrap();
let rows = stmt
.query_map([], |row| {
Ok(BackupHistoryRow {
id: row.get(0)?,
backup_path: row.get(1)?,
status: row.get(2)?,
created_at: row.get(3)?,
size_bytes: row.get(4)?,
error_message: row.get(5)?,
})
})
.unwrap();
rows.filter_map(|r| r.ok()).collect()
};
let csrf_token = generate_csrf_token(&session_id);
let template = crate::templates::BackupsTemplate {
admin_username: user.username,
files,
history,
csrf_token,
success: params.get("success").cloned(),
error: params.get("error").cloned(),
};
template.into_response()
}
// POST /admin/backups/create
pub async fn backups_create_post(
State(state): State<AppState>,
jar: CookieJar,
Form(form): Form<HashMap<String, String>>,
) -> Response {
let (_user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let form_csrf = form.get("csrf_token").cloned().unwrap_or_default();
if !verify_csrf(&session_id, &form_csrf) {
return Redirect::to("/admin/backups?error=Invalid CSRF token").into_response();
}
match crate::jobs::backup::perform_backup(&state.db, &state.config).await {
Ok(path) => {
let filename = std::path::Path::new(&path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("backup.tar.gz");
Redirect::to(&format!(
"/admin/backups?success=Backup created successfully: {}",
filename
))
.into_response()
}
Err(e) => Redirect::to(&format!(
"/admin/backups?error=Failed to generate backup: {}",
e
))
.into_response(),
}
}
// GET /admin/backups/download/:filename
pub async fn backups_download_get(
State(state): State<AppState>,
jar: CookieJar,
Path(filename): Path<String>,
) -> Response {
if require_auth(&state, &jar).await.is_err() {
return StatusCode::UNAUTHORIZED.into_response();
}
if filename.contains('/') || filename.contains('\\') || filename.contains("..") {
return StatusCode::BAD_REQUEST.into_response();
}
let backup_path = state.config.backup_dir.join(&filename);
if !backup_path.exists() {
return StatusCode::NOT_FOUND.into_response();
}
match std::fs::read(&backup_path) {
Ok(bytes) => {
let body = axum::body::Body::from(bytes);
Response::builder()
.header("content-type", "application/octet-stream")
.header(
"content-disposition",
format!("attachment; filename=\"{}\"", filename),
)
.body(body)
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
// POST /admin/backups/delete/:filename
pub async fn backups_delete_post(
State(state): State<AppState>,
jar: CookieJar,
Path(filename): Path<String>,
Form(form): Form<HashMap<String, String>>,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let form_csrf = form.get("csrf_token").cloned().unwrap_or_default();
if !verify_csrf(&session_id, &form_csrf) {
return Redirect::to("/admin/backups?error=Invalid CSRF token").into_response();
}
if filename.contains('/') || filename.contains('\\') || filename.contains("..") {
return Redirect::to("/admin/backups?error=Invalid filename").into_response();
}
let backup_path = state.config.backup_dir.join(&filename);
if !backup_path.exists() {
return Redirect::to("/admin/backups?error=Backup file not found").into_response();
}
match std::fs::remove_file(&backup_path) {
Ok(_) => {
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::audit_events::write_audit_event(
&system_conn,
&user.username,
"BACKUP_DELETE",
"backup",
&filename,
None,
);
Redirect::to("/admin/backups?success=Backup archive deleted").into_response()
}
Err(e) => Redirect::to(&format!(
"/admin/backups?error=Failed to delete backup file: {}",
e
))
.into_response(),
}
}
// POST /admin/backups/restore
pub async fn backups_restore_post(
State(state): State<AppState>,
jar: CookieJar,
mut multipart: axum::extract::Multipart,
) -> Response {
let (user, session_id) = match require_auth(&state, &jar).await {
Ok(u) => u,
Err(redir) => return redir.into_response(),
};
let mut backup_bytes = vec![];
let mut confirm_text = String::new();
let mut csrf_token = String::new();
while let Ok(Some(field)) = multipart.next_field().await {
let name = field.name().unwrap_or_default().to_string();
if name == "backup_file" {
if let Ok(bytes) = field.bytes().await {
backup_bytes = bytes.to_vec();
}
} else if name == "confirm_text" {
if let Ok(text) = field.text().await {
confirm_text = text.trim().to_string();
}
} else if name == "csrf_token" {
if let Ok(text) = field.text().await {
csrf_token = text.trim().to_string();
}
}
}
if !verify_csrf(&session_id, &csrf_token) {
return Redirect::to("/admin/backups?error=Invalid CSRF token").into_response();
}
if confirm_text != "RESTORE" {
return Redirect::to(
"/admin/backups?error=Confirmation text mismatch. Please type RESTORE.",
)
.into_response();
}
if backup_bytes.is_empty() {
return Redirect::to("/admin/backups?error=Backup file is empty or missing.")
.into_response();
}
let temp_file_path = state.config.data_dir.join("temp-restore-upload.tar.gz");
if let Err(e) = std::fs::write(&temp_file_path, &backup_bytes) {
return Redirect::to(&format!(
"/admin/backups?error=Failed to save uploaded file: {}",
e
))
.into_response();
}
match crate::cli::restore::run(
temp_file_path.to_string_lossy().to_string(),
None,
state.config.clone(),
)
.await
{
Ok(_) => {
let _ = std::fs::remove_file(&temp_file_path);
let conn = state.users_db.lock().unwrap();
let _ = conn.execute("DELETE FROM sessions;", []);
let system_conn = state.system_db.lock().unwrap();
let _ = crate::db::audit_events::write_audit_event(
&system_conn,
&user.username,
"RESTORE_EXECUTION",
"backup",
"upload",
None,
);
Redirect::to("/admin/login?success=Restore successful. Please log in again.")
.into_response()
}
Err(e) => {
let _ = std::fs::remove_file(&temp_file_path);
Redirect::to(&format!("/admin/backups?error=Restore failed: {}", e)).into_response()
}
}
}