1use crate::Result;
46use std::path::{Path, PathBuf};
47
48const MAX_BACKUP_SIZE: u64 = 100 * 1024 * 1024;
50
51fn verify_real_directory(path: &Path) -> Result<()> {
56 if path
57 .symlink_metadata()
58 .map_err(|e| crate::Error::BackupError(format!("cannot stat {}: {}", path.display(), e)))?
59 .file_type()
60 .is_symlink()
61 {
62 return Err(crate::Error::BackupError(format!(
63 "backup directory is a symlink: {} (symlink attacks not allowed)",
64 path.display()
65 )));
66 }
67 Ok(())
68}
69
70pub struct BackupManager {
75 backup_dir: PathBuf,
76}
77
78impl BackupManager {
79 pub fn new(backup_dir: PathBuf) -> Result<Self> {
88 std::fs::create_dir_all(&backup_dir).map_err(|e| {
89 crate::Error::BackupError(format!("Failed to create backup directory: {}", e))
90 })?;
91 verify_real_directory(&backup_dir)?;
92 Ok(Self { backup_dir })
93 }
94
95 #[cfg(unix)]
112 fn copy_permissions(src: &Path, dst: &Path) -> std::io::Result<()> {
113 let src_meta = std::fs::symlink_metadata(src)?;
114 std::fs::set_permissions(dst, src_meta.permissions())?;
115 Ok(())
116 }
117
118 #[cfg(not(unix))]
119 fn copy_permissions(_src: &Path, _dst: &Path) -> std::io::Result<()> {
120 Ok(())
121 }
122
123 fn calculate_size(path: &Path) -> std::io::Result<u64> {
129 let meta = path.symlink_metadata()?;
130 if meta.file_type().is_symlink() {
131 return Err(std::io::Error::new(
132 std::io::ErrorKind::InvalidInput,
133 format!(
134 "symlink detected: {} (symlinks not allowed)",
135 path.display()
136 ),
137 ));
138 }
139 if meta.is_file() {
140 Ok(meta.len())
141 } else if meta.is_dir() {
142 let mut total: u64 = 0;
143 for entry in std::fs::read_dir(path)? {
144 let entry = entry?;
145 total = total.saturating_add(Self::calculate_size(&entry.path())?);
146 }
147 Ok(total)
148 } else {
149 Ok(0)
150 }
151 }
152
153 fn verify_integrity(src: &Path, dst: &Path) -> std::io::Result<()> {
159 let src_size = Self::calculate_size(src)?;
160 let dst_size = Self::calculate_size(dst)?;
161 if src_size != dst_size {
162 return Err(std::io::Error::new(
163 std::io::ErrorKind::InvalidData,
164 format!(
165 "backup integrity check failed: source={} bytes, backup={} bytes",
166 src_size, dst_size
167 ),
168 ));
169 }
170 Ok(())
171 }
172
173 fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
186 let metadata = src.symlink_metadata()?;
187 if metadata.file_type().is_symlink() {
188 return Err(std::io::Error::new(
189 std::io::ErrorKind::InvalidInput,
190 format!(
191 "symlink detected: {} (symlinks not allowed for security)",
192 src.display()
193 ),
194 ));
195 }
196
197 if src.is_dir() {
198 std::fs::create_dir_all(dst)?;
199 for entry in std::fs::read_dir(src)? {
200 let entry = entry?;
201 let src_path = entry.path();
202 let dst_path = dst.join(entry.file_name());
203 Self::copy_recursive(&src_path, &dst_path)?;
204 }
205 Self::copy_permissions(src, dst)?;
206 } else {
207 std::fs::copy(src, dst)?;
208 }
209 Ok(())
210 }
211
212 pub fn create_backup(&self, file_path: &Path, job_id: &str) -> Result<PathBuf> {
233 if file_path.symlink_metadata().is_err() {
234 return Err(crate::Error::BackupError("File does not exist".to_string()));
235 }
236
237 let size = Self::calculate_size(file_path)
238 .map_err(|e| crate::Error::BackupError(format!("Cannot calculate size: {}", e)))?;
239 if size > MAX_BACKUP_SIZE {
240 return Err(crate::Error::BackupError(format!(
241 "Backup size {} bytes exceeds limit of {} bytes (100MB)",
242 size, MAX_BACKUP_SIZE
243 )));
244 }
245
246 let base_name = file_path
247 .file_name()
248 .ok_or_else(|| crate::Error::BackupError("Invalid filename".to_string()))?;
249
250 let job_dir = self.backup_dir.join(job_id);
251 let parent = job_dir.clone();
252 std::fs::create_dir_all(&parent).map_err(|e| {
253 crate::Error::BackupError(format!("Failed to create backup directory: {}", e))
254 })?;
255
256 let backup_path = {
257 let candidate = job_dir.join(base_name);
258 if candidate.symlink_metadata().is_err() {
259 candidate
260 } else {
261 let mut counter: u32 = 1;
262 loop {
263 let suffixed =
264 job_dir.join(format!("{}.{}", base_name.to_string_lossy(), counter));
265 if suffixed.symlink_metadata().is_err() {
266 break suffixed;
267 }
268 counter = counter.saturating_add(1);
269 }
270 }
271 };
272
273 Self::copy_recursive(file_path, &backup_path)
274 .map_err(|e| crate::Error::BackupError(e.to_string()))?;
275
276 Self::verify_integrity(file_path, &backup_path)
277 .map_err(|e| crate::Error::BackupError(format!("Integrity check failed: {}", e)))?;
278
279 Ok(backup_path)
280 }
281
282 pub fn restore(&self, backup_path: &Path, target_path: &Path) -> Result<()> {
294 if backup_path.symlink_metadata().is_err() {
295 return Err(crate::Error::BackupError(
296 "Backup does not exist".to_string(),
297 ));
298 }
299
300 if target_path.symlink_metadata().is_ok() {
301 let pre_restore_dir = target_path
302 .parent()
303 .map_or_else(|| PathBuf::from("."), |p| p.to_path_buf())
304 .join(".runtimo_pre_restore");
305 std::fs::create_dir_all(&pre_restore_dir).map_err(|e| {
306 crate::Error::BackupError(format!("Cannot create pre-restore backup dir: {}", e))
307 })?;
308 let target_name = target_path
309 .file_name()
310 .unwrap_or_else(|| std::ffi::OsStr::new("target"));
311 let pre_restore_path = pre_restore_dir.join(target_name);
312 let _ = std::fs::remove_dir_all(&pre_restore_path);
313 let _ = std::fs::remove_file(&pre_restore_path);
314 Self::copy_recursive(target_path, &pre_restore_path).map_err(|e| {
315 crate::Error::BackupError(format!("Pre-restore backup failed: {}", e))
316 })?;
317 }
318
319 Self::copy_recursive(backup_path, target_path)
320 .map_err(|e| crate::Error::BackupError(e.to_string()))?;
321
322 Ok(())
323 }
324
325 pub fn cleanup(&self, older_than_secs: u64) -> Result<()> {
341 use std::time::{SystemTime, UNIX_EPOCH};
342
343 let cutoff = SystemTime::now()
344 .duration_since(UNIX_EPOCH)
345 .unwrap_or_default()
346 .as_secs()
347 .saturating_sub(older_than_secs);
348
349 if self.backup_dir.symlink_metadata().is_err() {
350 return Ok(());
351 }
352
353 for entry in std::fs::read_dir(&self.backup_dir)
354 .map_err(|e| crate::Error::BackupError(e.to_string()))?
355 {
356 let entry = entry.map_err(|e| crate::Error::BackupError(e.to_string()))?;
357 let path = entry.path();
358
359 let meta = path
360 .symlink_metadata()
361 .map_err(|e| crate::Error::BackupError(e.to_string()))?;
362 if meta.file_type().is_symlink() {
363 continue;
364 }
365 if !meta.is_dir() {
366 continue;
367 }
368
369 let mtime = meta
370 .modified()
371 .map_err(|e| crate::Error::BackupError(e.to_string()))?
372 .duration_since(UNIX_EPOCH)
373 .unwrap_or_default()
374 .as_secs();
375
376 if mtime < cutoff {
377 std::fs::remove_dir_all(&path)
378 .map_err(|e| crate::Error::BackupError(e.to_string()))?;
379 }
380 }
381
382 Ok(())
383 }
384}
385
386#[cfg(test)]
387#[allow(clippy::unused_result_ok, clippy::unwrap_used)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_new_creates_directory() {
393 let dir = std::env::temp_dir().join("runtimo_backup_test_new");
394 let _ = std::fs::remove_dir_all(&dir);
395 let result = BackupManager::new(dir.clone());
396 assert!(result.is_ok(), "should create directory");
397 assert!(dir.exists());
398 std::fs::remove_dir_all(&dir).ok();
399 }
400
401 #[test]
402 fn test_rejects_symlink_backup_dir() {
403 let target = std::env::temp_dir().join("runtimo_backup_target");
404 let link = std::env::temp_dir().join("runtimo_backup_link");
405 let _ = std::fs::remove_dir_all(&target);
406 let _ = std::fs::remove_file(&link);
407
408 std::fs::create_dir_all(&target).unwrap();
409
410 #[cfg(unix)]
411 {
412 use std::os::unix::fs::symlink;
413 if symlink(&target, &link).is_ok() {
414 let result = BackupManager::new(link.clone());
415 assert!(
416 result.is_err(),
417 "BackupManager should reject symlink backup directory"
418 );
419 let err = result.err().unwrap().to_string();
420 assert!(
421 err.contains("symlink"),
422 "error should mention symlink: {}",
423 err
424 );
425 std::fs::remove_file(&link).ok();
426 }
427 }
428
429 std::fs::remove_dir_all(&target).ok();
430 }
431
432 #[test]
433 fn test_verify_real_directory() {
434 let dir = std::env::temp_dir().join("runtimo_verify_test");
435 let _ = std::fs::remove_dir_all(&dir);
436 std::fs::create_dir_all(&dir).unwrap();
437
438 let result = verify_real_directory(&dir);
439 assert!(result.is_ok(), "real directory should pass: {:?}", result);
440
441 std::fs::remove_dir_all(&dir).ok();
442 }
443
444 #[test]
445 fn test_backup_directory() {
446 use crate::backup::BackupManager;
447
448 let backup_dir = std::env::temp_dir().join("runtimo_backup_dir_test");
449 let source_dir = std::env::temp_dir().join("runtimo_source_dir_test");
450 let _ = std::fs::remove_dir_all(&backup_dir);
451 let _ = std::fs::remove_dir_all(&source_dir);
452
453 std::fs::create_dir_all(&source_dir).unwrap();
454 std::fs::write(source_dir.join("file1.txt"), "content1").unwrap();
455 std::fs::write(source_dir.join("file2.txt"), "content2").unwrap();
456
457 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
458 let result = mgr.create_backup(&source_dir, "job123");
459
460 assert!(result.is_ok(), "should backup directory: {:?}", result);
461 let backup_path = result.unwrap();
462 assert!(backup_path.exists());
463 assert!(backup_path.join("file1.txt").exists());
464 assert!(backup_path.join("file2.txt").exists());
465
466 let content1 = std::fs::read_to_string(backup_path.join("file1.txt")).unwrap();
467 assert_eq!(content1, "content1");
468
469 std::fs::remove_dir_all(&backup_dir).ok();
470 std::fs::remove_dir_all(&source_dir).ok();
471 }
472
473 #[test]
474 fn test_backup_rejects_symlinks() {
475 use crate::backup::BackupManager;
476
477 let backup_dir = std::env::temp_dir().join("runtimo_backup_symlink_test");
478 let source_dir = std::env::temp_dir().join("runtimo_source_symlink_test");
479 let _ = std::fs::remove_dir_all(&backup_dir);
480 let _ = std::fs::remove_dir_all(&source_dir);
481
482 std::fs::create_dir_all(&source_dir).unwrap();
483 std::fs::write(source_dir.join("file.txt"), "content").unwrap();
484
485 #[cfg(unix)]
486 {
487 use std::os::unix::fs::symlink;
488 let symlink_path = source_dir.join("evil_symlink");
489 if symlink("/etc/passwd", &symlink_path).is_ok() {
490 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
491 let result = mgr.create_backup(&source_dir, "job123");
492 assert!(
493 result.is_err(),
494 "should reject directory containing symlinks"
495 );
496 let err = result.err().unwrap().to_string();
497 assert!(
498 err.contains("symlink"),
499 "error should mention symlink: {}",
500 err
501 );
502 }
503 }
504
505 std::fs::remove_dir_all(&backup_dir).ok();
506 std::fs::remove_dir_all(&source_dir).ok();
507 }
508
509 #[test]
510 fn test_restore_directory() {
511 use crate::backup::BackupManager;
512
513 let backup_dir = std::env::temp_dir().join("runtimo_restore_backup_test");
514 let source_dir = std::env::temp_dir().join("runtimo_restore_source_test");
515 let restore_dir = std::env::temp_dir().join("runtimo_restore_target_test");
516 let _ = std::fs::remove_dir_all(&backup_dir);
517 let _ = std::fs::remove_dir_all(&source_dir);
518 let _ = std::fs::remove_dir_all(&restore_dir);
519
520 std::fs::create_dir_all(&source_dir).unwrap();
521 std::fs::write(source_dir.join("file1.txt"), "content1").unwrap();
522 std::fs::write(source_dir.join("file2.txt"), "content2").unwrap();
523
524 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
525 let backup_result = mgr.create_backup(&source_dir, "job123");
526 assert!(backup_result.is_ok());
527 let backup_path = backup_result.unwrap();
528
529 let restore_result = mgr.restore(&backup_path, &restore_dir);
530 assert!(
531 restore_result.is_ok(),
532 "should restore directory: {:?}",
533 restore_result
534 );
535 assert!(restore_dir.join("file1.txt").exists());
536 assert!(restore_dir.join("file2.txt").exists());
537
538 let content1 = std::fs::read_to_string(restore_dir.join("file1.txt")).unwrap();
539 assert_eq!(content1, "content1");
540
541 std::fs::remove_dir_all(&backup_dir).ok();
542 std::fs::remove_dir_all(&source_dir).ok();
543 std::fs::remove_dir_all(&restore_dir).ok();
544 }
545
546 #[test]
547 #[cfg(unix)]
548 fn test_backup_preserves_executable_bit() {
549 use crate::backup::BackupManager;
550 use std::os::unix::fs::PermissionsExt;
551
552 let backup_dir = std::env::temp_dir().join("runtimo_backup_exec_test");
553 let source_dir = std::env::temp_dir().join("runtimo_source_exec_test");
554 let _ = std::fs::remove_dir_all(&backup_dir);
555 let _ = std::fs::remove_dir_all(&source_dir);
556
557 std::fs::create_dir_all(&source_dir).unwrap();
558 let script_path = source_dir.join("script.sh");
559 std::fs::write(&script_path, "#!/bin/bash\necho hello").unwrap();
560 let mut perms = std::fs::metadata(&script_path).unwrap().permissions();
561 perms.set_mode(0o755);
562 std::fs::set_permissions(&script_path, perms).unwrap();
563
564 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
565 let result = mgr.create_backup(&source_dir, "job123");
566 assert!(result.is_ok());
567
568 let backup_path = result.unwrap();
569 let backup_script = backup_path.join("script.sh");
570 let backup_perms = std::fs::metadata(backup_script).unwrap().permissions();
571 assert!(
572 backup_perms.mode() & 0o111 == 0o111,
573 "executable bit should be preserved"
574 );
575
576 std::fs::remove_dir_all(&backup_dir).ok();
577 std::fs::remove_dir_all(&source_dir).ok();
578 }
579
580 #[test]
581 fn test_restore_creates_pre_restore_backup() {
582 let backup_dir = std::env::temp_dir().join("runtimo_pre_restore_test");
583 let source_dir = std::env::temp_dir().join("runtimo_pre_restore_source");
584 let target_dir = std::env::temp_dir().join("runtimo_pre_restore_target");
585 let _ = std::fs::remove_dir_all(&backup_dir);
586 let _ = std::fs::remove_dir_all(&source_dir);
587 let _ = std::fs::remove_dir_all(&target_dir);
588
589 std::fs::create_dir_all(&source_dir).unwrap();
590 std::fs::write(source_dir.join("original.txt"), "original").unwrap();
591
592 std::fs::create_dir_all(&target_dir).unwrap();
593 std::fs::write(target_dir.join("original.txt"), "newer_data").unwrap();
594
595 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
596 let backup_path = mgr.create_backup(&source_dir, "job123").unwrap();
597
598 let restore_result = mgr.restore(&backup_path, &target_dir);
599 assert!(restore_result.is_ok());
600
601 let content = std::fs::read_to_string(target_dir.join("original.txt")).unwrap();
602 assert_eq!(content, "original");
603
604 let pre_restore_path = target_dir
605 .parent()
606 .unwrap()
607 .join(".runtimo_pre_restore")
608 .join("runtimo_pre_restore_target");
609 assert!(pre_restore_path.exists(), "pre-restore backup should exist");
610 let pre_restore_content =
611 std::fs::read_to_string(pre_restore_path.join("original.txt")).unwrap();
612 assert_eq!(
613 pre_restore_content, "newer_data",
614 "pre-restore should have the newer data"
615 );
616
617 std::fs::remove_dir_all(&backup_dir).ok();
618 std::fs::remove_dir_all(&source_dir).ok();
619 std::fs::remove_dir_all(&target_dir).ok();
620 let _ = std::fs::remove_dir_all(target_dir.parent().unwrap().join(".runtimo_pre_restore"));
621 }
622
623 #[test]
624 fn test_create_backup_size_limit_constant() {
625 assert_eq!(
626 MAX_BACKUP_SIZE,
627 100 * 1024 * 1024,
628 "MAX_BACKUP_SIZE should be 100MB"
629 );
630
631 let backup_dir = std::env::temp_dir().join("runtimo_size_limit_test");
632 let source_file = std::env::temp_dir().join("runtimo_size_limit_source");
633 let _ = std::fs::remove_dir_all(&backup_dir);
634 let _ = std::fs::remove_file(&source_file);
635
636 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
637 std::fs::write(&source_file, "small content").unwrap();
638 let result = mgr.create_backup(&source_file, "job123");
639 assert!(result.is_ok(), "small file should succeed");
640
641 std::fs::remove_dir_all(&backup_dir).ok();
642 std::fs::remove_file(&source_file).ok();
643 }
644
645 #[test]
646 fn test_cleanup_skips_symlinks() {
647 let backup_dir = std::env::temp_dir().join("runtimo_cleanup_symlink_test");
648 let real_target = std::env::temp_dir().join("runtimo_cleanup_real_target");
649 let _ = std::fs::remove_dir_all(&backup_dir);
650 let _ = std::fs::remove_dir_all(&real_target);
651
652 std::fs::create_dir_all(&backup_dir).unwrap();
653 std::fs::create_dir_all(&real_target).unwrap();
654 std::fs::write(real_target.join("important.txt"), "do not delete").unwrap();
655
656 #[cfg(unix)]
657 {
658 use std::os::unix::fs::symlink;
659 let symlink_path = backup_dir.join("evil_link");
660 if symlink(&real_target, &symlink_path).is_ok() {
661 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
662 let result = mgr.cleanup(0);
663 assert!(result.is_ok(), "cleanup should succeed even with symlinks");
664
665 assert!(
666 real_target.join("important.txt").exists(),
667 "symlink target should not be deleted"
668 );
669
670 std::fs::remove_file(&symlink_path).ok();
671 }
672 }
673
674 std::fs::remove_dir_all(&backup_dir).ok();
675 std::fs::remove_dir_all(&real_target).ok();
676 }
677
678 #[test]
679 fn test_backup_integrity_verification() {
680 let backup_dir = std::env::temp_dir().join("runtimo_integrity_test");
681 let source_file = std::env::temp_dir().join("runtimo_integrity_source");
682 let _ = std::fs::remove_dir_all(&backup_dir);
683 let _ = std::fs::remove_file(&source_file);
684
685 std::fs::write(&source_file, "integrity test content").unwrap();
686
687 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
688 let backup_path = mgr.create_backup(&source_file, "job123").unwrap();
689
690 let src_meta = std::fs::symlink_metadata(&source_file).unwrap();
691 let bak_meta = std::fs::symlink_metadata(&backup_path).unwrap();
692 assert_eq!(
693 src_meta.len(),
694 bak_meta.len(),
695 "backup size should match source"
696 );
697
698 std::fs::remove_dir_all(&backup_dir).ok();
699 std::fs::remove_file(&source_file).ok();
700 }
701
702 #[test]
705 fn test_backup_unicode_filename() {
706 let backup_dir = std::env::temp_dir().join("runtimo_backup_unicode_test");
707 let source_dir = std::env::temp_dir().join("runtimo_source_unicode_test");
708 let _ = std::fs::remove_dir_all(&backup_dir);
709 let _ = std::fs::remove_dir_all(&source_dir);
710
711 std::fs::create_dir_all(&source_dir).unwrap();
712
713 let emoji_file = source_dir.join("🚀rocket.txt");
715 std::fs::write(&emoji_file, "emoji content").unwrap();
716
717 let cjk_file = source_dir.join("你好世界.txt");
719 std::fs::write(&cjk_file, "cjk content").unwrap();
720
721 let arabic_file = source_dir.join("مرحبا.txt");
723 std::fs::write(&arabic_file, "arabic content").unwrap();
724
725 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
726
727 let emoji_backup = mgr.create_backup(&emoji_file, "unicode-job");
729 assert!(
730 emoji_backup.is_ok(),
731 "emoji backup failed: {:?}",
732 emoji_backup.err()
733 );
734 assert!(emoji_backup.unwrap().exists());
735
736 let cjk_backup = mgr.create_backup(&cjk_file, "unicode-job");
737 assert!(
738 cjk_backup.is_ok(),
739 "CJK backup failed: {:?}",
740 cjk_backup.err()
741 );
742 assert!(cjk_backup.unwrap().exists());
743
744 let arabic_backup = mgr.create_backup(&arabic_file, "unicode-job");
745 assert!(
746 arabic_backup.is_ok(),
747 "Arabic backup failed: {:?}",
748 arabic_backup.err()
749 );
750 assert!(arabic_backup.unwrap().exists());
751
752 let _ = std::fs::remove_file(&emoji_file);
754 let _ = std::fs::remove_file(&cjk_file);
755 let _ = std::fs::remove_file(&arabic_file);
756
757 let job_dir = backup_dir.join("unicode-job");
758 mgr.restore(&job_dir.join("🚀rocket.txt"), &emoji_file)
759 .unwrap();
760 mgr.restore(&job_dir.join("你好世界.txt"), &cjk_file)
761 .unwrap();
762 mgr.restore(&job_dir.join("مرحبا.txt"), &arabic_file)
763 .unwrap();
764
765 assert_eq!(
766 std::fs::read_to_string(&emoji_file).unwrap(),
767 "emoji content"
768 );
769 assert_eq!(std::fs::read_to_string(&cjk_file).unwrap(), "cjk content");
770 assert_eq!(
771 std::fs::read_to_string(&arabic_file).unwrap(),
772 "arabic content"
773 );
774
775 std::fs::remove_dir_all(&backup_dir).ok();
776 std::fs::remove_dir_all(&source_dir).ok();
777 }
778
779 #[test]
780 fn test_backup_unicode_directory_name() {
781 let backup_dir = std::env::temp_dir().join("runtimo_backup_unicode_dir_test");
783 let source_dir = std::env::temp_dir().join("source_🌟_目录");
784 let _ = std::fs::remove_dir_all(&backup_dir);
785 let _ = std::fs::remove_dir_all(&source_dir);
786
787 std::fs::create_dir_all(&source_dir).unwrap();
788 std::fs::write(source_dir.join("inner.txt"), "inside unicode dir").unwrap();
789
790 let mgr = BackupManager::new(backup_dir.clone()).unwrap();
791 let backup = mgr.create_backup(&source_dir, "unicode-dir-job");
792 assert!(
793 backup.is_ok(),
794 "Unicode dir backup failed: {:?}",
795 backup.err()
796 );
797 assert!(backup.unwrap().join("inner.txt").exists());
798
799 std::fs::remove_dir_all(&backup_dir).ok();
800 std::fs::remove_dir_all(&source_dir).ok();
801 }
802}