@@ -307,6 +307,8 @@ pub struct ImageImporter {
307307 offline : bool ,
308308 /// If true, we have ostree v2024.3 or newer.
309309 ostree_v2024_3 : bool ,
310+ /// Mapping from diff_id to blob digest for layer deduplication
311+ diffid_to_digest : HashMap < String , String > ,
310312
311313 layer_progress : Option < Sender < ImportProgress > > ,
312314 layer_byte_progress : Option < tokio:: sync:: watch:: Sender < Option < LayerProgress > > > ,
@@ -641,6 +643,9 @@ impl ImageImporter {
641643 ) ;
642644
643645 let repo = repo. clone ( ) ;
646+
647+ let diffid_to_digest = Self :: build_diffid_to_digest_map ( & repo) ?;
648+
644649 Ok ( ImageImporter {
645650 repo,
646651 proxy,
@@ -651,6 +656,7 @@ impl ImageImporter {
651656 require_bootable : false ,
652657 offline : false ,
653658 imgref : imgref. clone ( ) ,
659+ diffid_to_digest,
654660 layer_progress : None ,
655661 layer_byte_progress : None ,
656662 } )
@@ -749,6 +755,111 @@ impl ImageImporter {
749755 . await
750756 }
751757
758+ /// Build a mapping from diff_id to blob_digest by enumerating all stored images.
759+ /// This allows us to reuse layers with the same content even if they have different blob digests.
760+ fn build_diffid_to_digest_map ( repo : & ostree:: Repo ) -> Result < HashMap < String , String > > {
761+ let mut map = HashMap :: new ( ) ;
762+ let all_images = list_images ( repo) ?;
763+
764+ for imgref_str in all_images {
765+ let imgref = match ImageReference :: try_from ( imgref_str. as_str ( ) ) {
766+ Ok ( r) => r,
767+ Err ( e) => {
768+ tracing:: warn!( "Failed to parse image reference {}: {}" , imgref_str, e) ;
769+ continue ;
770+ }
771+ } ;
772+
773+ let state = match query_image ( repo, & imgref) ? {
774+ Some ( s) => s,
775+ None => continue ,
776+ } ;
777+
778+ // Map each layer's diff_id to its blob digest
779+ for ( layer_desc, diff_id) in state
780+ . manifest
781+ . layers ( )
782+ . iter ( )
783+ . zip ( state. configuration . rootfs ( ) . diff_ids ( ) )
784+ {
785+ let diff_id_str = diff_id. to_string ( ) ;
786+ // Only store first found
787+ map. entry ( diff_id_str)
788+ . or_insert_with ( || layer_desc. digest ( ) . to_string ( ) ) ;
789+ }
790+ }
791+
792+ Ok ( map)
793+ }
794+
795+ fn find_digest_by_diffid (
796+ & self ,
797+ manifest : & oci_image:: ImageManifest ,
798+ config : & oci_image:: ImageConfiguration ,
799+ layer : & oci_image:: Descriptor ,
800+ ) -> Option < & String > {
801+ let idx = manifest
802+ . layers ( )
803+ . iter ( )
804+ . position ( |l| l. digest ( ) == layer. digest ( ) ) ?;
805+ let layer_diffid = config. rootfs ( ) . diff_ids ( ) . get ( idx) ?;
806+ self . diffid_to_digest . get ( layer_diffid. as_str ( ) )
807+ }
808+
809+ /// Try to resolve a layer commit by looking up its diff_id in already-imported images.
810+ fn resolve_commit_by_diffid (
811+ & self ,
812+ manifest : & oci_image:: ImageManifest ,
813+ config : & oci_image:: ImageConfiguration ,
814+ layer : & oci_image:: Descriptor ,
815+ ) -> Result < Option < String > > {
816+ let Some ( existing_digest) = self . find_digest_by_diffid ( manifest, config, layer) else {
817+ return Ok ( None ) ;
818+ } ;
819+ let existing_ref = ref_for_blob_digest ( existing_digest) ?;
820+ Ok ( self
821+ . repo
822+ . resolve_rev ( & existing_ref, true ) ?
823+ . map ( |s| s. to_string ( ) ) )
824+ }
825+
826+ /// Query a layer by digest, falling back to diff_id lookup if the direct
827+ /// ref is not found.
828+ fn query_layer (
829+ & self ,
830+ manifest : & oci_image:: ImageManifest ,
831+ config : & oci_image:: ImageConfiguration ,
832+ layer : & oci_image:: Descriptor ,
833+ ) -> Result < ManifestLayerState > {
834+ let ostree_ref = ref_for_layer ( layer) ?;
835+ let commit = self
836+ . repo
837+ . resolve_rev ( & ostree_ref, true ) ?
838+ . map ( |s| s. to_string ( ) ) ;
839+ // If no direct ref match, try to find a layer with the same diff_id
840+ // but a different blob digest (e.g. due to recompression).
841+ let commit = match commit {
842+ Some ( c) => Some ( c) ,
843+ None => self . resolve_commit_by_diffid ( manifest, config, layer) ?,
844+ } ;
845+
846+ Ok ( ManifestLayerState {
847+ layer : layer. clone ( ) ,
848+ ostree_ref,
849+ commit,
850+ } )
851+ }
852+
853+ /// Ensure a ref exists for a layer, creating it if needed.
854+ fn ensure_ref_for_layer ( repo : & ostree:: Repo , ostree_ref : & str , commit : & str ) -> Result < ( ) > {
855+ let ref_exists = repo. resolve_rev ( ostree_ref, true ) ?. is_some ( ) ;
856+ if !ref_exists {
857+ tracing:: debug!( "Creating ref {} for reused commit {}" , ostree_ref, commit) ;
858+ repo. set_ref_immediate ( None , ostree_ref, Some ( commit) , gio:: Cancellable :: NONE ) ?;
859+ }
860+ Ok ( ( ) )
861+ }
862+
752863 /// Given existing metadata (manifest, config, previous image statE) generate a PreparedImport structure
753864 /// which e.g. includes a diff of the layers.
754865 fn create_prepared_import (
@@ -779,15 +890,18 @@ impl ImageImporter {
779890 let ( commit_layer, component_layers, remaining_layers) =
780891 parse_manifest_layout ( & manifest, & config) ?;
781892
782- let query = |l : & Descriptor | query_layer ( & self . repo , l. clone ( ) ) ;
783- let commit_layer = commit_layer. map ( query) . transpose ( ) ?;
893+ let commit_layer = commit_layer
894+ . map ( |layer| self . query_layer ( & manifest, & config, layer) )
895+ . transpose ( ) ?;
896+
784897 let component_layers = component_layers
785898 . into_iter ( )
786- . map ( query )
899+ . map ( |l| self . query_layer ( & manifest , & config , l ) )
787900 . collect :: < Result < Vec < _ > > > ( ) ?;
901+
788902 let remaining_layers = remaining_layers
789903 . into_iter ( )
790- . map ( query )
904+ . map ( |l| self . query_layer ( & manifest , & config , l ) )
791905 . collect :: < Result < Vec < _ > > > ( ) ?;
792906
793907 let previous_manifest_digest = previous_state. as_ref ( ) . map ( |s| s. manifest_digest . clone ( ) ) ;
@@ -937,7 +1051,10 @@ impl ImageImporter {
9371051 } ;
9381052 let des_layers = self . proxy . get_layer_info ( & import. proxy_img ) . await ?;
9391053 for layer in import. ostree_layers . iter_mut ( ) {
940- if layer. commit . is_some ( ) {
1054+ if let Some ( commit) = layer. commit . as_ref ( ) {
1055+ if write_refs {
1056+ Self :: ensure_ref_for_layer ( & self . repo , & layer. ostree_ref , commit) ?;
1057+ }
9411058 continue ;
9421059 }
9431060 if let Some ( p) = self . layer_progress . as_ref ( ) {
@@ -984,7 +1101,11 @@ impl ImageImporter {
9841101 . await ?;
9851102 }
9861103 }
987- if commit_layer. commit . is_none ( ) {
1104+ if let Some ( commit) = commit_layer. commit . as_ref ( ) {
1105+ if write_refs {
1106+ Self :: ensure_ref_for_layer ( & self . repo , & commit_layer. ostree_ref , commit) ?;
1107+ }
1108+ } else {
9881109 if let Some ( p) = self . layer_progress . as_ref ( ) {
9891110 p. send ( ImportProgress :: OstreeChunkStarted (
9901111 commit_layer. layer . clone ( ) ,
@@ -1031,7 +1152,7 @@ impl ImageImporter {
10311152 ) )
10321153 . await ?;
10331154 }
1034- } ;
1155+ }
10351156 Ok ( ( ) )
10361157 }
10371158
@@ -1234,6 +1355,8 @@ impl ImageImporter {
12341355 for layer in import. layers {
12351356 if let Some ( c) = layer. commit {
12361357 tracing:: debug!( "Reusing fetched commit {}" , c) ;
1358+ Self :: ensure_ref_for_layer ( & self . repo , & layer. ostree_ref , & c) ?;
1359+
12371360 layer_commits. push ( c. to_string ( ) ) ;
12381361 } else {
12391362 if let Some ( p) = self . layer_progress . as_ref ( ) {
0 commit comments