1use crate::{
2 container::DockerManager,
3 database::{BackupRecord, BackupStatus, BackupType, Database},
4 error::DuckError,
5};
6use anyhow::Result;
7use chrono::Utc;
8use flate2::Compression;
9use flate2::read::GzDecoder;
10use flate2::write::GzEncoder;
11use std::path::{Path, PathBuf};
12use std::{fs::File, sync::Arc};
13use tar::Archive;
14use tar::Builder;
15use tracing::{debug, error, info, warn};
16use walkdir::WalkDir;
17
18#[derive(Debug, Clone)]
20pub struct BackupManager {
21 storage_dir: PathBuf,
22 database: Arc<Database>,
23 docker_manager: Arc<DockerManager>,
24}
25
26#[derive(Debug, Clone)]
28pub struct BackupOptions {
29 pub backup_type: BackupType,
31 pub service_version: String,
33 pub work_dir: PathBuf,
35 pub source_paths: Vec<PathBuf>,
37 pub compression_level: u32,
39}
40
41#[derive(Debug, Clone)]
43pub struct RestoreOptions {
44 pub target_dir: PathBuf,
46 pub force_overwrite: bool,
48}
49
50impl BackupManager {
51 pub fn new(
53 storage_dir: PathBuf,
54 database: Arc<Database>,
55 docker_manager: Arc<DockerManager>,
56 ) -> Result<Self> {
57 if !storage_dir.exists() {
58 std::fs::create_dir_all(&storage_dir)?;
59 }
60
61 Ok(Self {
62 storage_dir,
63 database,
64 docker_manager,
65 })
66 }
67
68 pub async fn create_backup(&self, options: BackupOptions) -> Result<BackupRecord> {
70 let need_backup_paths = options.source_paths;
72
73 let timestamp = Utc::now().format("%Y-%m-%d_%H-%M-%S");
75 let backup_type_str = match options.backup_type {
76 BackupType::Manual => "manual",
77 BackupType::PreUpgrade => "pre-upgrade",
78 };
79
80 let backup_filename = format!(
81 "backup_{}_v{}_{}.tar.gz",
82 backup_type_str, options.service_version, timestamp
83 );
84
85 let backup_path = self.storage_dir.join(&backup_filename);
86
87 info!("Starting to create backup: {}", backup_path.display());
88
89 match self
91 .perform_backup(&need_backup_paths, &backup_path, options.compression_level)
92 .await
93 {
94 Ok(_) => {
95 info!("Backup created successfully: {}", backup_path.display());
96
97 let record_id = self
99 .database
100 .create_backup_record(
101 backup_path.to_string_lossy().to_string(),
102 options.service_version,
103 options.backup_type,
104 BackupStatus::Completed,
105 )
106 .await?;
107
108 self.database
110 .get_backup_by_id(record_id)
111 .await?
112 .ok_or_else(|| anyhow::anyhow!("Cannot get the backup record just created"))
113 }
114 Err(e) => {
115 error!("Backup creation failed: {}", e);
116
117 self.database
119 .create_backup_record(
120 backup_path.to_string_lossy().to_string(),
121 options.service_version,
122 options.backup_type,
123 BackupStatus::Failed,
124 )
125 .await?;
126
127 Err(e)
128 }
129 }
130 }
131
132 async fn perform_backup(
138 &self,
139 source_paths: &[PathBuf],
140 backup_path: &Path,
141 compression_level: u32,
142 ) -> Result<()> {
143 if let Some(parent) = backup_path.parent() {
145 tokio::fs::create_dir_all(parent).await?;
146 }
147
148 let source_paths = source_paths.to_vec();
150 let backup_path = backup_path.to_path_buf();
151
152 tokio::task::spawn_blocking(move || {
153 let file = File::create(&backup_path)?;
154 let compression = Compression::new(compression_level);
155 let encoder = GzEncoder::new(file, compression);
156 let mut archive = Builder::new(encoder);
157
158 for source_path in &source_paths {
160 if source_path.is_file() {
161 add_file_to_archive(&mut archive, source_path, None)?;
163 } else if source_path.is_dir() {
164 let dir_name = source_path
165 .file_name()
166 .ok_or_else(|| anyhow::anyhow!("Cannot get directory name"))?
167 .to_string_lossy()
168 .to_string();
169
170 for entry in WalkDir::new(source_path) {
172 let entry = entry
173 .map_err(|e| anyhow::anyhow!("Failed to traverse directory: {e}"))?;
174 let path = entry.path();
175
176 if path.is_file() {
177 add_file_to_archive(
178 &mut archive,
179 path,
180 Some((source_path, &dir_name)),
181 )?;
182 }
183 }
184 } else {
185 info!(
187 "File or directory does not exist, no need to backup: {}",
188 source_path.display()
189 );
190 }
191 }
192
193 archive
194 .finish()
195 .map_err(|e| anyhow::anyhow!("Failed to finish archive: {e}"))?;
196
197 Ok::<(), anyhow::Error>(())
198 })
199 .await??;
200
201 Ok(())
202 }
203
204 pub async fn restore_data_from_backup_with_exculde(
206 &self,
207 backup_id: i64,
208 target_dir: &Path,
209 auto_start_service: bool,
210 dirs_to_exculde: &[&str],
211 ) -> Result<()> {
212 let backup_record = self
214 .database
215 .get_backup_by_id(backup_id)
216 .await?
217 .ok_or_else(|| anyhow::anyhow!("Backup record does not exist: {backup_id}"))?;
218
219 let backup_path = PathBuf::from(&backup_record.file_path);
220 if !backup_path.exists() {
221 return Err(anyhow::anyhow!(
222 "Backup file does not exist: {}",
223 backup_path.display()
224 ));
225 }
226
227 info!(
228 "Starting intelligent data restore: {}",
229 backup_path.display()
230 );
231 info!("Target directory: {}", target_dir.display());
232
233 info!("Stopping services...");
235 self.docker_manager.stop_services().await?;
236
237 self.clear_data_directories(target_dir, dirs_to_exculde)
239 .await?;
240
241 self.perform_restore(&backup_path, target_dir, dirs_to_exculde)
243 .await?;
244
245 if auto_start_service {
247 info!("Data restore completed, starting services...");
248 self.docker_manager.start_services().await?;
249 info!(
250 "Data restored and started successfully: {}",
251 target_dir.display()
252 );
253 } else {
254 info!("Data restore completed, skipping service start (controlled by parent process)");
255 info!("Data restored successfully: {}", target_dir.display());
256 }
257
258 Ok(())
259 }
260
261 pub async fn restore_data_directory_only(
263 &self,
264 backup_id: i64,
265 target_dir: &Path,
266 auto_start_service: bool,
267 dirs_to_restore: &[&str],
268 ) -> Result<()> {
269 let backup_record = self
271 .database
272 .get_backup_by_id(backup_id)
273 .await?
274 .ok_or_else(|| anyhow::anyhow!("Backup record does not exist: {backup_id}"))?;
275
276 let backup_path = PathBuf::from(&backup_record.file_path);
277 if !backup_path.exists() {
278 return Err(anyhow::anyhow!(
279 "Backup file does not exist: {}",
280 backup_path.display()
281 ));
282 }
283
284 info!("Starting data directory restore: {}", backup_path.display());
285 info!("Target directory: {}", target_dir.display());
286
287 info!("Stopping services...");
289 self.docker_manager.stop_services().await?;
290
291 self.clear_data_directory_only(target_dir).await?;
293
294 self.perform_selective_restore(&backup_path, target_dir, dirs_to_restore)
296 .await?;
297
298 if auto_start_service {
300 info!("Data directory restore completed, starting services...");
301 self.docker_manager.start_services().await?;
302 info!(
303 "Data directory restored and started successfully: {}",
304 target_dir.display()
305 );
306 } else {
307 info!(
308 "Data directory restore completed, skipping service start (controlled by parent process)"
309 );
310 info!(
311 "Data directory restored successfully: {}",
312 target_dir.display()
313 );
314 }
315
316 Ok(())
317 }
318
319 async fn clear_data_directories(
321 &self,
322 docker_dir: &Path,
323 dirs_to_exculde: &[&str],
324 ) -> Result<()> {
325 let mut data_dirs_to_clear: Vec<String> = vec!["data".to_string(), "app".to_string()];
326 data_dirs_to_clear.retain(|dir| !dirs_to_exculde.contains(&dir.as_str()));
328
329 for dir_name in data_dirs_to_clear.iter() {
330 let dir_path = docker_dir.join(dir_name);
331 if dir_path.exists() {
332 info!("Cleaning data directory: {}", dir_path.display());
333 self.force_remove_directory(&dir_path).await?;
334 }
335 }
336
337 info!("Data directory cleanup completed, config files preserved");
338 Ok(())
339 }
340
341 async fn force_remove_directory(&self, path: &Path) -> Result<()> {
343 if !path.exists() {
344 return Ok(());
345 }
346
347 info!("Force cleaning directory: {}", path.display());
348
349 if path.is_symlink() {
351 info!("Removing symbolic link: {}", path.display());
352 tokio::fs::remove_file(path).await?;
353 return Ok(());
354 }
355
356 let mut entries = match tokio::fs::read_dir(path).await {
358 Ok(entries) => entries,
359 Err(e) => {
360 warn!("Failed to read directory: {} - {}", path.display(), e);
361 return tokio::fs::remove_dir_all(path).await.map_err(|e| {
363 anyhow::anyhow!("Failed to delete directory: {} - {}", path.display(), e)
364 });
365 }
366 };
367
368 while let Some(entry) = entries.next_entry().await? {
369 let entry_path = entry.path();
370
371 if entry_path.is_symlink() {
372 info!("Removing symbolic link: {}", entry_path.display());
373 tokio::fs::remove_file(&entry_path).await?;
374 } else if entry_path.is_dir() {
375 Box::pin(self.force_remove_directory(&entry_path)).await?;
377
378 if let Err(e) = tokio::fs::remove_dir(&entry_path).await
380 && e.kind() != std::io::ErrorKind::NotFound
381 {
382 warn!(
383 "Failed to remove empty directory: {} - {}",
384 entry_path.display(),
385 e
386 );
387 }
388 } else {
389 if let Err(e) = tokio::fs::remove_file(&entry_path).await
390 && e.kind() != std::io::ErrorKind::NotFound
391 {
392 warn!("Failed to remove file: {} - {}", entry_path.display(), e);
393 }
394 }
395 }
396
397 if let Err(e) = tokio::fs::remove_dir(path).await
399 && e.kind() != std::io::ErrorKind::NotFound
400 {
401 warn!(
402 "Failed to remove root directory: {} - {}",
403 path.display(),
404 e
405 );
406 }
407
408 Ok(())
409 }
410
411 async fn clear_data_directory_only(&self, docker_dir: &Path) -> Result<()> {
413 let data_dir = docker_dir.join("data");
414 if data_dir.exists() {
415 info!("Cleaning data directory: {}", data_dir.display());
416 tokio::fs::remove_dir_all(&data_dir).await?;
417 }
418
419 info!("Data directory cleanup completed, app directory and config files preserved");
420 Ok(())
421 }
422
423 async fn perform_selective_restore(
425 &self,
426 backup_path: &Path,
427 target_dir: &Path,
428 dirs_to_restore: &[&str],
429 ) -> Result<()> {
430 use flate2::read::GzDecoder;
431 use std::fs::File;
432 use tar::Archive;
433
434 tokio::fs::create_dir_all(target_dir).await?;
436
437 let backup_path = backup_path.to_path_buf();
438 let target_dir = target_dir.to_path_buf();
439 let dirs_to_restore: Vec<String> = dirs_to_restore.iter().map(|s| s.to_string()).collect();
440
441 tokio::task::spawn_blocking(move || {
443 let file = File::open(&backup_path)?;
444 let decoder = GzDecoder::new(file);
445 let mut archive = Archive::new(decoder);
446
447 for entry in archive.entries()? {
449 let mut entry = entry
450 .map_err(|e| DuckError::Backup(format!("Failed to read archive entry: {e}")))?;
451
452 let entry_path = entry
454 .path()
455 .map_err(|e| DuckError::Backup(format!("Failed to get entry path: {e}")))?;
456 let entry_path_str = entry_path.to_string_lossy();
457
458 let should_restore = dirs_to_restore
460 .iter()
461 .any(|dir| entry_path_str.starts_with(&format!("{dir}/")));
462
463 if should_restore {
464 let target_path = target_dir.join(&*entry_path);
466
467 if let Some(parent) = target_path.parent() {
469 std::fs::create_dir_all(parent)?;
470 }
471
472 entry.unpack(&target_path).map_err(|e| {
474 DuckError::Backup(format!(
475 "Failed to unpack file {}: {e}",
476 target_path.display()
477 ))
478 })?;
479
480 debug!("Restoring file: {}", target_path.display());
481 }
482 }
483
484 Ok::<(), DuckError>(())
485 })
486 .await??;
487
488 Ok(())
489 }
490
491 async fn perform_restore(
493 &self,
494 backup_path: &Path,
495 target_dir: &Path,
496 dirs_to_exculde: &[&str],
497 ) -> Result<()> {
498 tokio::fs::create_dir_all(target_dir).await?;
500
501 let backup_path = backup_path.to_path_buf();
502 let target_dir = target_dir.to_path_buf();
503 let dirs_to_exclude: Vec<String> = dirs_to_exculde.iter().map(|s| s.to_string()).collect();
504
505 tokio::task::spawn_blocking(move || {
507 let file = File::open(&backup_path)?;
508 let decoder = GzDecoder::new(file);
509 let mut archive = Archive::new(decoder);
510
511 let mut debug_dirs = std::collections::HashSet::new();
512
513 for entry in archive.entries()? {
515 let mut entry = entry
516 .map_err(|e| DuckError::Backup(format!("Failed to read archive entry: {e}")))?;
517
518 let entry_path = entry
520 .path()
521 .map_err(|e| DuckError::Backup(format!("Failed to get entry path: {e}")))?;
522 let entry_path_str = entry_path.to_string_lossy();
523
524 let path_components: Vec<&str> = entry_path_str.split('/').collect();
526
527 let should_exclude = if !path_components.is_empty() {
529 let first_level_dir = path_components[0];
530 debug_dirs.insert(first_level_dir.to_string());
531
532 dirs_to_exclude
533 .iter()
534 .any(|dir| dir.as_str() == first_level_dir)
535 } else {
536 false };
538
539 if !should_exclude {
540 let target_path = target_dir.join(&*entry_path);
542
543 if let Some(parent) = target_path.parent() {
545 std::fs::create_dir_all(parent)?;
546 }
547
548 entry.unpack(&target_path).map_err(|e| {
550 DuckError::Backup(format!(
551 "Failed to unpack file {}: {e}",
552 target_path.display()
553 ))
554 })?;
555
556 debug!("Restoring file: {}", target_path.display());
557 }
558 }
559
560 debug!("Test log, restore directories: {:?}", debug_dirs);
561
562 Ok::<(), DuckError>(())
563 })
564 .await??;
565
566 Ok(())
567 }
568
569 pub async fn list_backups(&self) -> Result<Vec<BackupRecord>> {
571 self.database.get_all_backups().await
572 }
573
574 pub async fn delete_backup(&self, backup_id: i64) -> Result<()> {
576 let backup_record = self
578 .database
579 .get_backup_by_id(backup_id)
580 .await?
581 .ok_or_else(|| {
582 DuckError::Backup(format!("Backup record does not exist: {backup_id}"))
583 })?;
584
585 let backup_path = PathBuf::from(&backup_record.file_path);
586
587 if backup_path.exists() {
589 tokio::fs::remove_file(&backup_path).await?;
590 info!("Deleting backup file: {}", backup_path.display());
591 }
592
593 self.database.delete_backup_record(backup_id).await?;
595
596 Ok(())
597 }
598
599 pub async fn migrate_storage_directory(&self, new_storage_dir: &Path) -> Result<()> {
601 if new_storage_dir == self.storage_dir {
602 return Ok(()); }
604
605 info!(
606 "Starting to migrate backup storage directory: {} -> {}",
607 self.storage_dir.display(),
608 new_storage_dir.display()
609 );
610
611 tokio::fs::create_dir_all(new_storage_dir).await?;
613
614 let backups = self.list_backups().await?;
616
617 for backup in backups {
618 let old_path = PathBuf::from(&backup.file_path);
619 if old_path.exists() {
620 let filename = old_path
621 .file_name()
622 .ok_or_else(|| DuckError::Backup("Cannot get backup filename".to_string()))?;
623 let new_path = new_storage_dir.join(filename);
624
625 tokio::fs::rename(&old_path, &new_path).await?;
627 info!(
628 "Migrating backup file: {} -> {}",
629 old_path.display(),
630 new_path.display()
631 );
632
633 self.database
635 .update_backup_file_path(backup.id, new_path.to_string_lossy().to_string())
636 .await?;
637 }
638 }
639
640 info!("Backup storage directory migration completed");
641 Ok(())
642 }
643
644 pub fn get_storage_dir(&self) -> &Path {
646 &self.storage_dir
647 }
648
649 pub async fn estimate_backup_size(&self, source_dir: &Path) -> Result<u64> {
651 let source_dir = source_dir.to_path_buf();
652
653 let total_size = tokio::task::spawn_blocking(move || {
654 let mut total = 0u64;
655
656 for entry in WalkDir::new(&source_dir).into_iter().flatten() {
657 if entry.path().is_file()
658 && let Ok(metadata) = entry.metadata()
659 {
660 total += metadata.len();
661 }
662 }
663
664 total
665 })
666 .await?;
667
668 Ok(total_size / 2)
670 }
671}
672
673fn add_file_to_archive(
675 archive: &mut Builder<GzEncoder<File>>,
676 file_path: &Path,
677 base_info: Option<(&Path, &str)>,
678) -> Result<()> {
679 let archive_path = if let Some((base_dir, dir_name)) = base_info {
680 let relative_path = file_path
682 .strip_prefix(base_dir)
683 .map_err(|e| DuckError::Backup(format!("Failed to calculate relative path: {e}")))?;
684
685 if cfg!(windows) {
687 format!(
688 "{}/{}",
689 dir_name,
690 relative_path.display().to_string().replace('\\', "/")
691 )
692 } else {
693 format!("{}/{}", dir_name, relative_path.display())
694 }
695 } else {
696 let path_str = file_path.to_string_lossy().to_string();
698
699 let path_str = if cfg!(windows) {
701 path_str.replace('\\', "/")
702 } else {
703 path_str
704 };
705
706 if let Some(stripped) = path_str.strip_prefix("./") {
708 stripped.to_string()
709 } else {
710 path_str
711 }
712 };
713
714 debug!(
715 "添加文件到归档: {} -> {}",
716 file_path.display(),
717 archive_path
718 );
719
720 archive
721 .append_path_with_name(file_path, archive_path)
722 .map_err(|e| DuckError::Backup(format!("Failed to add file to archive: {e}")))?;
723
724 Ok(())
725}