From ae7ef10b67737f08a86743f687bca30a12e9e17a Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 12 Jul 2026 19:38:03 +0200 Subject: first stable --- src/routes/backups.rs | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 src/routes/backups.rs (limited to 'src/routes/backups.rs') diff --git a/src/routes/backups.rs b/src/routes/backups.rs new file mode 100644 index 0000000..09298d2 --- /dev/null +++ b/src/routes/backups.rs @@ -0,0 +1,256 @@ +use askama::Template; +use axum::{ + Router, + body::Body, + extract::{Path, State}, + http::{StatusCode, header}, + response::{Html, Redirect, Response}, + routing::{get, post}, +}; +use rusqlite::params; +use zip::write::FileOptions; + +use crate::auth::CurrentUser; +use crate::routes::AppState; + +#[derive(Template)] +#[template(path = "backups/list.html")] +struct BackupListTemplate { + username: String, + is_superuser: bool, + password_change_required: bool, + instance_name: String, + instance_id: i64, + backups: Vec, + message: Option, +} + +struct BackupRow { + id: i64, + filename: String, + created_at: String, +} + +pub fn router() -> Router { + Router::new() + .route("/instances/{id}/backups", get(list_backups)) + .route("/instances/{id}/backups/create", post(create_backup)) + .route("/backups/{id}/restore", post(restore_backup)) + .route("/backups/{id}/download", get(download_backup)) + .route("/instances/{id}/world/export", get(export_world)) +} + +async fn list_backups( + State(state): State, + Path(instance_id): Path, + CurrentUser(account): CurrentUser, +) -> Result, axum::response::Redirect> { + let conn = state.db.conn(); + let instance_name: String = conn + .query_row( + "SELECT name FROM instances WHERE id = ?1", + params![instance_id], + |row| row.get(0), + ) + .map_err(|_| Redirect::to("/instances"))?; + + let mut stmt = conn + .prepare("SELECT id, filename, created_at FROM backups WHERE instance_id = ?1 ORDER BY created_at DESC") + .map_err(|_| Redirect::to("/instances"))?; + + let backups: Vec = stmt + .query_map(params![instance_id], |row| { + Ok(BackupRow { + id: row.get(0)?, + filename: row.get(1)?, + created_at: row.get(2)?, + }) + }) + .map_err(|_| Redirect::to("/instances"))? + .filter_map(|r| r.ok()) + .collect(); + drop(stmt); + drop(conn); + + let template = BackupListTemplate { + username: account.username.clone(), + is_superuser: account.is_superuser, + password_change_required: account.password_change_required, + instance_name, + instance_id, + backups, + message: None, + }; + + Ok(Html(template.render().unwrap_or_default())) +} + +async fn create_backup( + State(state): State, + Path(instance_id): Path, + CurrentUser(_user): CurrentUser, +) -> Result { + let conn = state.db.conn(); + let instance_name: String = conn + .query_row( + "SELECT name FROM instances WHERE id = ?1", + params![instance_id], + |row| row.get(0), + ) + .map_err(|_| Redirect::to("/instances"))?; + drop(conn); + + let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); + let filename = format!("{instance_name}_{timestamp}.zip"); + + let data_dir = state + .config + .server_directory + .join("instances") + .join(&instance_name) + .join("data"); + + let backup_dir = state.config.server_directory.join("backups"); + let _ = std::fs::create_dir_all(&backup_dir); + let backup_path = backup_dir.join(&filename); + + if let Ok(file) = std::fs::File::create(&backup_path) { + let mut zip = zip::ZipWriter::new(file); + let options: FileOptions<'_, ()> = + FileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + if let Ok(entries) = std::fs::read_dir(&data_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + if let Some(name) = path.file_name() { + if let Ok(contents) = std::fs::read(&path) { + let _ = zip.start_file(&*name.to_string_lossy(), options); + let _ = std::io::Write::write_all(&mut zip, &contents); + } + } + } + } + } + let _ = zip.finish(); + } + + let conn = state.db.conn(); + conn.execute( + "INSERT INTO backups (instance_id, filename) VALUES (?1, ?2)", + params![instance_id, filename], + ) + .ok(); + drop(conn); + + Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))) +} + +async fn restore_backup( + State(state): State, + Path(backup_id): Path, + CurrentUser(_user): CurrentUser, +) -> Result { + let conn = state.db.conn(); + let (instance_id, _filename): (i64, String) = conn + .query_row( + "SELECT instance_id, filename FROM backups WHERE id = ?1", + params![backup_id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|_| Redirect::to("/instances"))?; + drop(conn); + + Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))) +} + +async fn download_backup( + State(state): State, + Path(backup_id): Path, + CurrentUser(_user): CurrentUser, +) -> Result { + let conn = state.db.conn(); + let (_, filename): (i64, String) = conn + .query_row( + "SELECT instance_id, filename FROM backups WHERE id = ?1", + params![backup_id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .map_err(|_| (StatusCode::NOT_FOUND, "Backup not found"))?; + drop(conn); + + let backup_path = state + .config + .server_directory + .join("backups") + .join(&filename); + + let content = + std::fs::read(&backup_path).map_err(|_| (StatusCode::NOT_FOUND, "File not found"))?; + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "application/zip") + .header( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{filename}\""), + ) + .body(Body::from(content)) + .unwrap()) +} + +async fn export_world( + State(state): State, + Path(instance_id): Path, + CurrentUser(_user): CurrentUser, +) -> Result { + let conn = state.db.conn(); + let instance_name: String = conn + .query_row( + "SELECT name FROM instances WHERE id = ?1", + params![instance_id], + |row| row.get(0), + ) + .map_err(|_| (StatusCode::NOT_FOUND, "Instance not found"))?; + drop(conn); + + let world_dir = state + .config + .server_directory + .join("instances") + .join(&instance_name) + .join("data") + .join("world"); + + let mut zip_buf = Vec::new(); + { + let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut zip_buf)); + let options: FileOptions<'_, ()> = + FileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + if let Ok(entries) = std::fs::read_dir(&world_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + if let Some(name) = path.file_name() { + if let Ok(contents) = std::fs::read(&path) { + let _ = zip.start_file(&*name.to_string_lossy(), options); + let _ = std::io::Write::write_all(&mut zip, &contents); + } + } + } + } + } + let _ = zip.finish(); + } + + let filename = format!("{instance_name}_world.zip"); + + Ok(Response::builder() + .header(header::CONTENT_TYPE, "application/zip") + .header( + header::CONTENT_DISPOSITION, + format!("attachment; filename=\"{filename}\""), + ) + .body(Body::from(zip_buf)) + .unwrap()) +} -- cgit v1.2.3