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.rs40
1 files changed, 28 insertions, 12 deletions
diff --git a/src/routes/backups.rs b/src/routes/backups.rs
index d68be8d..b55d5c8 100644
--- a/src/routes/backups.rs
+++ b/src/routes/backups.rs
@@ -12,6 +12,7 @@ use zip::write::FileOptions;
12 12
13use crate::auth::CurrentUser; 13use crate::auth::CurrentUser;
14use crate::routes::AppState; 14use crate::routes::AppState;
15use crate::sanitize;
15 16
16#[derive(Template)] 17#[derive(Template)]
17#[template(path = "backups/list.html")] 18#[template(path = "backups/list.html")]
@@ -97,20 +98,37 @@ async fn create_backup(
97 .map_err(|_| Redirect::to("/instances"))?; 98 .map_err(|_| Redirect::to("/instances"))?;
98 drop(conn); 99 drop(conn);
99 100
101 // Sanitize instance name to prevent path traversal
102 let safe_name = match sanitize::sanitize_instance_name(&instance_name) {
103 Some(n) => n,
104 None => return Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))),
105 };
106
100 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); 107 let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
101 let filename = format!("{instance_name}_{timestamp}.zip"); 108 let filename = format!("{safe_name}_{timestamp}.zip");
102 109
103 let data_dir = state 110 let data_dir = state
104 .config 111 .config
105 .server_directory 112 .server_directory
106 .join("instances") 113 .join("instances")
107 .join(&instance_name) 114 .join(&safe_name)
108 .join("data"); 115 .join("data");
109 116
110 let backup_dir = state.config.server_directory.join("backups"); 117 let backup_dir = state.config.server_directory.join("backups");
111 let _ = std::fs::create_dir_all(&backup_dir); 118 let _ = std::fs::create_dir_all(&backup_dir);
112 let backup_path = backup_dir.join(&filename); 119 let backup_path = backup_dir.join(&filename);
113 120
121 // FIX: Insert DB record BEFORE creating the ZIP file.
122 // This prevents orphaned backup files if the process crashes between ZIP creation and DB insert.
123 let conn = state.db.conn();
124 conn.execute(
125 "INSERT INTO backups (instance_id, filename) VALUES (?1, ?2)",
126 params![instance_id, filename],
127 )
128 .ok();
129 drop(conn);
130
131 // Now create the ZIP file (DB insert already succeeded)
114 if let Ok(file) = std::fs::File::create(&backup_path) { 132 if let Ok(file) = std::fs::File::create(&backup_path) {
115 let mut zip = zip::ZipWriter::new(file); 133 let mut zip = zip::ZipWriter::new(file);
116 let options: FileOptions<'_, ()> = 134 let options: FileOptions<'_, ()> =
@@ -130,14 +148,6 @@ async fn create_backup(
130 let _ = zip.finish(); 148 let _ = zip.finish();
131 } 149 }
132 150
133 let conn = state.db.conn();
134 conn.execute(
135 "INSERT INTO backups (instance_id, filename) VALUES (?1, ?2)",
136 params![instance_id, filename],
137 )
138 .ok();
139 drop(conn);
140
141 Ok(Redirect::to(&format!("/instances/{instance_id}/backups"))) 151 Ok(Redirect::to(&format!("/instances/{instance_id}/backups")))
142} 152}
143 153
@@ -174,11 +184,17 @@ async fn download_backup(
174 .map_err(|_| (StatusCode::NOT_FOUND, "Backup not found"))?; 184 .map_err(|_| (StatusCode::NOT_FOUND, "Backup not found"))?;
175 drop(conn); 185 drop(conn);
176 186
187 // Sanitize backup filename to prevent path traversal
188 let safe_filename = match sanitize::sanitize_backup_filename(&filename) {
189 Some(f) => f,
190 None => return Err((StatusCode::BAD_REQUEST, "Invalid backup filename")),
191 };
192
177 let backup_path = state 193 let backup_path = state
178 .config 194 .config
179 .server_directory 195 .server_directory
180 .join("backups") 196 .join("backups")
181 .join(&filename); 197 .join(&safe_filename);
182 198
183 let content = 199 let content =
184 std::fs::read(&backup_path).map_err(|_| (StatusCode::NOT_FOUND, "File not found"))?; 200 std::fs::read(&backup_path).map_err(|_| (StatusCode::NOT_FOUND, "File not found"))?;
@@ -187,7 +203,7 @@ async fn download_backup(
187 .header(header::CONTENT_TYPE, "application/zip") 203 .header(header::CONTENT_TYPE, "application/zip")
188 .header( 204 .header(
189 header::CONTENT_DISPOSITION, 205 header::CONTENT_DISPOSITION,
190 format!("attachment; filename=\"{filename}\""), 206 format!("attachment; filename=\"{safe_filename}\""),
191 ) 207 )
192 .body(Body::from(content)) 208 .body(Body::from(content))
193 .unwrap()) 209 .unwrap())