2525//! use composefs_oci::cstor::import_from_containers_storage;
2626//!
2727//! let repo = Arc::new(Repository::open_user()?);
28- //! let result = import_from_containers_storage(&repo, "sha256:abc123...", None).await?;
28+ //! let ( result, stats) = import_from_containers_storage(&repo, "sha256:abc123...", None).await?;
2929//! println!("Imported config: {}", result.0);
30+ //! println!("Stats: {:?}", stats);
3031//! ```
3132
3233use std:: io:: { Read , Seek , SeekFrom } ;
@@ -38,13 +39,67 @@ use indicatif::{ProgressBar, ProgressStyle};
3839use sha2:: Digest ;
3940use tokio:: task:: spawn_blocking;
4041
41- use composefs:: { fsverity:: FsVerityHashValue , repository:: Repository , INLINE_CONTENT_MAX } ;
42+ use composefs:: {
43+ fsverity:: FsVerityHashValue ,
44+ repository:: { ObjectStoreMethod , Repository } ,
45+ INLINE_CONTENT_MAX ,
46+ } ;
4247
4348use cstorage:: { Image , Layer , Storage , TarSplitFdStream , TarSplitItem } ;
4449
4550use crate :: skopeo:: { OCI_CONFIG_CONTENT_TYPE , TAR_LAYER_CONTENT_TYPE } ;
4651use crate :: { config_identifier, layer_identifier, ContentAndVerity } ;
4752
53+ /// Statistics from a containers-storage import operation.
54+ #[ derive( Debug , Clone , Default ) ]
55+ pub struct ImportStats {
56+ /// Number of layers in the image.
57+ pub layers : u64 ,
58+ /// Number of layers that were already present (skipped).
59+ pub layers_already_present : u64 ,
60+ /// Number of objects stored via reflink (zero-copy).
61+ pub objects_reflinked : u64 ,
62+ /// Number of objects stored via regular copy (reflink not supported).
63+ pub objects_copied : u64 ,
64+ /// Number of objects that were already present (deduplicated).
65+ pub objects_already_present : u64 ,
66+ /// Total bytes stored via reflink.
67+ pub bytes_reflinked : u64 ,
68+ /// Total bytes stored via regular copy.
69+ pub bytes_copied : u64 ,
70+ /// Total bytes inlined in splitstreams (small files + headers).
71+ pub bytes_inlined : u64 ,
72+ }
73+
74+ impl ImportStats {
75+ /// Merge stats from another ImportStats into this one.
76+ pub fn merge ( & mut self , other : & ImportStats ) {
77+ self . layers += other. layers ;
78+ self . layers_already_present += other. layers_already_present ;
79+ self . objects_reflinked += other. objects_reflinked ;
80+ self . objects_copied += other. objects_copied ;
81+ self . objects_already_present += other. objects_already_present ;
82+ self . bytes_reflinked += other. bytes_reflinked ;
83+ self . bytes_copied += other. bytes_copied ;
84+ self . bytes_inlined += other. bytes_inlined ;
85+ }
86+
87+ /// Returns true if any objects were stored via reflink.
88+ pub fn used_reflinks ( & self ) -> bool {
89+ self . objects_reflinked > 0
90+ }
91+
92+ /// Total number of objects processed.
93+ pub fn total_objects ( & self ) -> u64 {
94+ self . objects_reflinked + self . objects_copied + self . objects_already_present
95+ }
96+
97+ /// Total bytes processed (external objects only, not inline).
98+ pub fn total_external_bytes ( & self ) -> u64 {
99+ self . bytes_reflinked + self . bytes_copied
100+ }
101+ }
102+
48103/// Import a container image from containers-storage into the composefs repository.
49104///
50105/// This function reads an image from the local containers-storage (podman/buildah)
@@ -56,12 +111,12 @@ use crate::{config_identifier, layer_identifier, ContentAndVerity};
56111/// * `reference` - Optional reference name to assign to the imported config
57112///
58113/// # Returns
59- /// A tuple of (config_digest, config_verity_id) for the imported image .
114+ /// A tuple of (( config_digest, config_verity_id), import_stats) .
60115pub async fn import_from_containers_storage < ObjectID : FsVerityHashValue > (
61116 repo : & Arc < Repository < ObjectID > > ,
62117 image_id : & str ,
63118 reference : Option < & str > ,
64- ) -> Result < ContentAndVerity < ObjectID > > {
119+ ) -> Result < ( ContentAndVerity < ObjectID > , ImportStats ) > {
65120 let repo = Arc :: clone ( repo) ;
66121 let image_id = image_id. to_owned ( ) ;
67122 let reference = reference. map ( |s| s. to_owned ( ) ) ;
@@ -81,7 +136,9 @@ fn import_from_containers_storage_blocking<ObjectID: FsVerityHashValue>(
81136 repo : & Arc < Repository < ObjectID > > ,
82137 image_id : & str ,
83138 reference : Option < & str > ,
84- ) -> Result < ContentAndVerity < ObjectID > > {
139+ ) -> Result < ( ContentAndVerity < ObjectID > , ImportStats ) > {
140+ let mut stats = ImportStats :: default ( ) ;
141+
85142 // Open containers-storage
86143 let storage = Storage :: discover ( ) . context ( "Failed to discover containers-storage" ) ?;
87144
@@ -112,6 +169,8 @@ fn import_from_containers_storage_blocking<ObjectID: FsVerityHashValue>(
112169 diff_ids. len( )
113170 ) ;
114171
172+ stats. layers = storage_layer_ids. len ( ) as u64 ;
173+
115174 // Import each layer with progress bar
116175 let progress = ProgressBar :: new ( storage_layer_ids. len ( ) as u64 ) ;
117176 progress. set_style (
@@ -128,12 +187,15 @@ fn import_from_containers_storage_blocking<ObjectID: FsVerityHashValue>(
128187
129188 let layer_verity = if let Some ( existing) = repo. has_stream ( & content_id) ? {
130189 progress. set_message ( format ! ( "Already have {short_id}..." ) ) ;
190+ stats. layers_already_present += 1 ;
131191 existing
132192 } else {
133193 progress. set_message ( format ! ( "Importing {short_id}..." ) ) ;
134194 let layer = Layer :: open ( & storage, storage_layer_id)
135195 . with_context ( || format ! ( "Failed to open layer {}" , storage_layer_id) ) ?;
136- import_layer_with_writer ( repo, & storage, & layer, diff_id) ?
196+ let ( verity, layer_stats) = import_layer_with_writer ( repo, & storage, & layer, diff_id) ?;
197+ stats. merge ( & layer_stats) ;
198+ verity
137199 } ;
138200
139201 layer_refs. push ( ( diff_id. clone ( ) , layer_verity) ) ;
@@ -165,11 +227,12 @@ fn import_from_containers_storage_blocking<ObjectID: FsVerityHashValue>(
165227
166228 // Write config inline
167229 writer. write_inline ( & config_json) ;
230+ stats. bytes_inlined += config_json. len ( ) as u64 ;
168231
169232 repo. write_stream ( writer, & content_id, reference) ?
170233 } ;
171234
172- Ok ( ( config_digest, config_verity) )
235+ Ok ( ( ( config_digest, config_verity) , stats ) )
173236}
174237
175238/// Import a single layer from containers-storage using the writer pattern.
@@ -178,12 +241,16 @@ fn import_from_containers_storage_blocking<ObjectID: FsVerityHashValue>(
178241/// - For large files: reflinks the file content to the objects directory
179242/// - For small files: embeds content inline in the splitstream
180243/// - Writes tar headers and padding as inline data
244+ ///
245+ /// Returns the layer's verity ID and import statistics for this layer.
181246fn import_layer_with_writer < ObjectID : FsVerityHashValue > (
182247 repo : & Arc < Repository < ObjectID > > ,
183248 storage : & Storage ,
184249 layer : & Layer ,
185250 diff_id : & str ,
186- ) -> Result < ObjectID > {
251+ ) -> Result < ( ObjectID , ImportStats ) > {
252+ let mut stats = ImportStats :: default ( ) ;
253+
187254 let mut stream = TarSplitFdStream :: new ( storage, layer)
188255 . with_context ( || format ! ( "Failed to create tar-split stream for layer {}" , layer. id( ) ) ) ?;
189256
@@ -194,6 +261,7 @@ fn import_layer_with_writer<ObjectID: FsVerityHashValue>(
194261 match item {
195262 TarSplitItem :: Segment ( bytes) => {
196263 // Write raw segment bytes (tar headers, padding) as inline data
264+ stats. bytes_inlined += bytes. len ( ) as u64 ;
197265 writer. write_inline ( & bytes) ;
198266 }
199267 TarSplitItem :: FileContent { fd, size, name } => {
@@ -202,10 +270,24 @@ fn import_layer_with_writer<ObjectID: FsVerityHashValue>(
202270
203271 if size as usize > INLINE_CONTENT_MAX {
204272 // Large file: use reflink to store as external object
205- let object_id = repo
206- . ensure_object_from_file ( & file, size)
273+ let ( object_id, method ) = repo
274+ . ensure_object_from_file_with_stats ( & file, size)
207275 . with_context ( || format ! ( "Failed to store object for {}" , name) ) ?;
208276
277+ match method {
278+ ObjectStoreMethod :: Reflinked => {
279+ stats. objects_reflinked += 1 ;
280+ stats. bytes_reflinked += size;
281+ }
282+ ObjectStoreMethod :: Copied => {
283+ stats. objects_copied += 1 ;
284+ stats. bytes_copied += size;
285+ }
286+ ObjectStoreMethod :: AlreadyPresent => {
287+ stats. objects_already_present += 1 ;
288+ }
289+ }
290+
209291 writer. add_external_size ( size) ;
210292 writer. write_reference ( object_id) ?;
211293 } else {
@@ -214,14 +296,16 @@ fn import_layer_with_writer<ObjectID: FsVerityHashValue>(
214296 let mut file = file;
215297 file. seek ( SeekFrom :: Start ( 0 ) ) ?;
216298 file. read_exact ( & mut content) ?;
299+ stats. bytes_inlined += size;
217300 writer. write_inline ( & content) ;
218301 }
219302 }
220303 }
221304 }
222305
223306 // Write the stream with the content identifier
224- repo. write_stream ( writer, & content_id, None )
307+ let verity = repo. write_stream ( writer, & content_id, None ) ?;
308+ Ok ( ( verity, stats) )
225309}
226310
227311/// Check if an image reference uses the containers-storage transport.
0 commit comments