summaryrefslogtreecommitdiffstats
path: root/src/routes/backups.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes/backups.rs')
-rw-r--r--src/routes/backups.rs256
1 files changed, 256 insertions, 0 deletions
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 @@
1use askama::Template;
2use axum::{
3 Router,
4 body::Body,
5 extract::{Path, State},
6 http::{StatusCode, header},
7 response::{Html, Redirect, Response},
8 routing::{get, post},
9};
10use rusqlite::params;
11use zip::write::FileOptions;
12
13use crate::auth::CurrentUser;
14use crate::routes::AppState;
15
16#[derive(Template)]
17#[template(path = "backups/list.html")]
18struct BackupListTemplate {
19 username: String,
20 is_superuser: bool,
21 password_change_required: bool,
22 instance_name: String,
23 instance_id: i64,
24 backups: Vec<BackupRow>,
25 message: Option<String>,
26}
27
28struct BackupRow {
29 id: i64,
30 filename: String,
31 created_at: String,
32}
33
34pub fn router() -> Router<AppState> {
35 Router::new()
36 .route("/instances/{id}/backups", get(list_backups))
37 .route("/instances/{id}/backups/create", post(create_backup))
38 .route("/backups/{id}/restore", post(restore_backup))
39 .route("/backups/{id}/download", get(download_backup))
40 .route("/instances/{id}/world/export", get(export_world))
41}
42
43async fn list_backups(
44 State(state): State<AppState>,
45 Path(instance_id): Path<i64>,
46 CurrentUser(account): CurrentUser,
47) -> Result<Html<String>, axum::response::Redirect> {
48 let conn = state.db.conn();
49 let instance_name: String = conn
50 .query_row(
51 "SELECT name FROM instances WHERE id = ?1",
52 params![instance_id],
53 |row| row.get(0),
54 )
55 .map_err(|_| Redirect::to("/instances"))?;
56
57 let mut stmt = conn
58 .prepare("SELECT id, filename, created_at FROM backups WHERE instance_id = ?1 ORDER BY created_at DESC")
59 .map_err(|_| Redirect::to("/instances"))?;
60
61 let backups: Vec<BackupRow> = stmt
62 .query_map(params![instance_id], |row| {
63 Ok(BackupRow {
64 id: row.get(0)?,
65 filename: row.get(1)?,
66 created_at: row.get(2)?,
67 })
68 })
69 .map_err(|_| Redirect::to("/instances"))?
70 .filter_map(|r| r.ok())
71 .collect();
72 drop(stmt);
73 drop(conn);
74
75 let template = BackupListTemplate {
76 username: account.username.clone(),
77 is_superuser: account.is_superuser,
78 password_change_required: account.password_change_required,
79 instance_name,
80 instance_id,
81 backups,
82 message: None,
83 };
84
85 Ok(Html(template.render().unwrap_or_default()))
86}
87
88async fn create_backup(
89 State(state): State<AppState>,
90 Path(instance_id): Path<i64>,
91 CurrentUser(_user): CurrentUser,
92) -> Result<Redirect, axum::response::Redirect> {
93 let conn = state.db.conn();
94 let instance_name: String = conn
95 .query_row(
96 "SELECT name FROM instances WHERE id = ?1",
97 params![instance_id],
98 |row| row.get(0),
99 )
100 .map_err(|_| Redirect::to("/instances"))?;
101 drop(conn);
102
103 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
104 let filename = format!("{instance_name}_{timestamp}.zip");
105
106 let data_dir = state
107 .config
108 .server_directory
109 .join("instances")
110 .join(&instance_name)
111 .join("data");
112
113 let backup_dir = state.config.server_directory.join("backups");
114 let _ = std::fs::create_dir_all(&backup_dir);
115 let backup_path = backup_dir.join(&filename);
116
117 if let Ok(file) = std::fs::File::create(&backup_path) {
118 let mut zip = zip::ZipWriter::new(file);
119 let options: FileOptions<'_, ()> =
120 FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
121
122 if let Ok(entries) = std::fs::read_dir(&data_dir) {
123 for entry in entries.flatten() {
124 let path = entry.path();
125 if path.is_file() {
126 if let Some(name) = path.file_name() {
127 if let Ok(contents) = std::fs::read(&path) {
128 let _ = zip.start_file(&*name.to_string_lossy(), options);
129 let _ = std::io::Write::write_all(&mut zip, &contents);
130 }
131 }
132 }
133 }
134 }
135 let _ = zip.finish();
136 }
137
138 let conn = state.db.conn();
139 conn.execute(
140 "INSERT INTO backups (instance_id, filename) VALUES (?1, ?2)",
141 params![instance_id, filename],
142 )
143 .ok();
144 drop(conn);
145
146 Ok(Redirect::to(&format!("/instances/{instance_id}/backups")))
147}
148
149async fn restore_backup(
150 State(state): State<AppState>,
151 Path(backup_id): Path<i64>,
152 CurrentUser(_user): CurrentUser,
153) -> Result<Redirect, axum::response::Redirect> {
154 let conn = state.db.conn();
155 let (instance_id, _filename): (i64, String) = conn
156 .query_row(
157 "SELECT instance_id, filename FROM backups WHERE id = ?1",
158 params![backup_id],
159 |row| Ok((row.get(0)?, row.get(1)?)),
160 )
161 .map_err(|_| Redirect::to("/instances"))?;
162 drop(conn);
163
164 Ok(Redirect::to(&format!("/instances/{instance_id}/backups")))
165}
166
167async fn download_backup(
168 State(state): State<AppState>,
169 Path(backup_id): Path<i64>,
170 CurrentUser(_user): CurrentUser,
171) -> Result<Response, (StatusCode, &'static str)> {
172 let conn = state.db.conn();
173 let (_, filename): (i64, String) = conn
174 .query_row(
175 "SELECT instance_id, filename FROM backups WHERE id = ?1",
176 params![backup_id],
177 |row| Ok((row.get(0)?, row.get(1)?)),
178 )
179 .map_err(|_| (StatusCode::NOT_FOUND, "Backup not found"))?;
180 drop(conn);
181
182 let backup_path = state
183 .config
184 .server_directory
185 .join("backups")
186 .join(&filename);
187
188 let content =
189 std::fs::read(&backup_path).map_err(|_| (StatusCode::NOT_FOUND, "File not found"))?;
190
191 Ok(Response::builder()
192 .header(header::CONTENT_TYPE, "application/zip")
193 .header(
194 header::CONTENT_DISPOSITION,
195 format!("attachment; filename=\"{filename}\""),
196 )
197 .body(Body::from(content))
198 .unwrap())
199}
200
201async fn export_world(
202 State(state): State<AppState>,
203 Path(instance_id): Path<i64>,
204 CurrentUser(_user): CurrentUser,
205) -> Result<Response, (StatusCode, &'static str)> {
206 let conn = state.db.conn();
207 let instance_name: String = conn
208 .query_row(
209 "SELECT name FROM instances WHERE id = ?1",
210 params![instance_id],
211 |row| row.get(0),
212 )
213 .map_err(|_| (StatusCode::NOT_FOUND, "Instance not found"))?;
214 drop(conn);
215
216 let world_dir = state
217 .config
218 .server_directory
219 .join("instances")
220 .join(&instance_name)
221 .join("data")
222 .join("world");
223
224 let mut zip_buf = Vec::new();
225 {
226 let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut zip_buf));
227 let options: FileOptions<'_, ()> =
228 FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
229
230 if let Ok(entries) = std::fs::read_dir(&world_dir) {
231 for entry in entries.flatten() {
232 let path = entry.path();
233 if path.is_file() {
234 if let Some(name) = path.file_name() {
235 if let Ok(contents) = std::fs::read(&path) {
236 let _ = zip.start_file(&*name.to_string_lossy(), options);
237 let _ = std::io::Write::write_all(&mut zip, &contents);
238 }
239 }
240 }
241 }
242 }
243 let _ = zip.finish();
244 }
245
246 let filename = format!("{instance_name}_world.zip");
247
248 Ok(Response::builder()
249 .header(header::CONTENT_TYPE, "application/zip")
250 .header(
251 header::CONTENT_DISPOSITION,
252 format!("attachment; filename=\"{filename}\""),
253 )
254 .body(Body::from(zip_buf))
255 .unwrap())
256}