@@ -447,7 +447,7 @@ impl FlatPathBackup {
447447 }
448448 }
449449 Ok ( metadata) if metadata. is_dir ( ) => {
450- hardlink_copy_tree ( & path, backup) ?;
450+ backup_directory_tree ( & path, backup) ?;
451451 FlatPathState :: Directory {
452452 backup : backup. to_path_buf ( ) ,
453453 }
@@ -496,6 +496,79 @@ impl FlatPathBackup {
496496 }
497497}
498498
499+ fn backup_directory_tree ( source : & Path , destination : & Path ) -> Result < ( ) , FcError > {
500+ let parent = destination_parent ( destination) ?;
501+ fs:: create_dir_all ( parent) . map_err ( |source_err| FcError :: PathIo {
502+ path : parent. to_path_buf ( ) ,
503+ source : source_err,
504+ } ) ?;
505+ let tmp = sibling_tmp_path ( destination, "backup-tree" ) ;
506+ remove_path_if_exists ( & tmp) ?;
507+ copy_backup_tree_contents ( source, & tmp) ?;
508+ if let Err ( err) = replace_path_with_prepared_tree ( & tmp, destination) {
509+ let _ = remove_path_if_exists ( & tmp) ;
510+ return Err ( err) ;
511+ }
512+ Ok ( ( ) )
513+ }
514+
515+ fn copy_backup_tree_contents ( source : & Path , destination : & Path ) -> Result < ( ) , FcError > {
516+ fs:: create_dir ( destination) . map_err ( |source_err| FcError :: PathIo {
517+ path : destination. to_path_buf ( ) ,
518+ source : source_err,
519+ } ) ?;
520+ fs:: set_permissions ( destination, fs:: Permissions :: from_mode ( 0o755 ) ) . map_err ( |source_err| {
521+ FcError :: PathIo {
522+ path : destination. to_path_buf ( ) ,
523+ source : source_err,
524+ }
525+ } ) ?;
526+ for entry in fs:: read_dir ( source) . map_err ( |source_err| FcError :: PathIo {
527+ path : source. to_path_buf ( ) ,
528+ source : source_err,
529+ } ) ? {
530+ let entry = entry. map_err ( |source_err| FcError :: PathIo {
531+ path : source. to_path_buf ( ) ,
532+ source : source_err,
533+ } ) ?;
534+ let source_path = entry. path ( ) ;
535+ let destination_path = destination. join ( entry. file_name ( ) ) ;
536+ let metadata =
537+ fs:: symlink_metadata ( & source_path) . map_err ( |source_err| FcError :: PathIo {
538+ path : source_path. clone ( ) ,
539+ source : source_err,
540+ } ) ?;
541+ if metadata. file_type ( ) . is_symlink ( ) {
542+ let target = fs:: read_link ( & source_path) . map_err ( |source_err| FcError :: PathIo {
543+ path : source_path. clone ( ) ,
544+ source : source_err,
545+ } ) ?;
546+ symlink ( & target, & destination_path) . map_err ( |source_err| FcError :: PathIo {
547+ path : destination_path,
548+ source : source_err,
549+ } ) ?;
550+ } else if metadata. is_dir ( ) {
551+ copy_backup_tree_contents ( & source_path, & destination_path) ?;
552+ } else if metadata. is_file ( ) {
553+ fs:: hard_link ( & source_path, & destination_path) . map_err ( |source_err| {
554+ FcError :: PathIo {
555+ path : destination_path,
556+ source : source_err,
557+ }
558+ } ) ?;
559+ } else {
560+ return Err ( FcError :: Config ( ConfigError :: InvalidValue {
561+ field : "install.flat_projection" ,
562+ reason : format ! (
563+ "flat projection backup contains unsupported file type: {}" ,
564+ source_path. display( )
565+ ) ,
566+ } ) ) ;
567+ }
568+ }
569+ Ok ( ( ) )
570+ }
571+
499572fn flat_managed_paths ( install_root : & Path ) -> Vec < PathBuf > {
500573 let bin_dir = install_root. join ( "bin" ) ;
501574 let artifact_dir = install_root. join ( "artifacts" ) ;
@@ -519,7 +592,7 @@ mod tests {
519592 use std:: fs;
520593 use std:: os:: unix:: fs:: MetadataExt ;
521594
522- use super :: hardlink_copy_tree;
595+ use super :: { backup_directory_tree , hardlink_copy_tree} ;
523596
524597 #[ test]
525598 fn hardlink_copy_tree_replaces_stale_destination_entries ( ) {
@@ -549,6 +622,31 @@ mod tests {
549622 ) ;
550623 }
551624
625+ #[ test]
626+ fn backup_directory_tree_preserves_legacy_symlinks_without_following ( ) {
627+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
628+ let source = temp. path ( ) . join ( "source" ) ;
629+ let destination = temp. path ( ) . join ( "backup" ) ;
630+ fs:: create_dir_all ( & source) . unwrap ( ) ;
631+ fs:: write ( source. join ( "manifest.json" ) , b"manifest" ) . unwrap ( ) ;
632+ std:: os:: unix:: fs:: symlink (
633+ "../versions/v0.2.20/artifacts/release-proof-cache" ,
634+ source. join ( "release-proof-cache" ) ,
635+ )
636+ . unwrap ( ) ;
637+
638+ backup_directory_tree ( & source, & destination) . unwrap ( ) ;
639+
640+ assert_same_inode (
641+ & source. join ( "manifest.json" ) ,
642+ & destination. join ( "manifest.json" ) ,
643+ ) ;
644+ assert_eq ! (
645+ fs:: read_link( destination. join( "release-proof-cache" ) ) . unwrap( ) ,
646+ std:: path:: PathBuf :: from( "../versions/v0.2.20/artifacts/release-proof-cache" )
647+ ) ;
648+ }
649+
552650 fn assert_same_inode ( left : & std:: path:: Path , right : & std:: path:: Path ) {
553651 let left = fs:: metadata ( left) . unwrap ( ) ;
554652 let right = fs:: metadata ( right) . unwrap ( ) ;
0 commit comments