66
77use std:: collections:: HashMap ;
88use std:: path:: { Path , PathBuf } ;
9+ use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
910use std:: sync:: Arc ;
1011
1112use a3s_box_core:: error:: { BoxError , Result } ;
@@ -14,6 +15,9 @@ use chrono::Utc;
1415use serde:: { Deserialize , Serialize } ;
1516use tokio:: sync:: RwLock ;
1617
18+ /// Per-process counter for unique staging-dir names in `put`.
19+ static PUT_SEQ : AtomicU64 = AtomicU64 :: new ( 0 ) ;
20+
1721/// Persistent index stored as JSON on disk.
1822#[ derive( Debug , Default , Serialize , Deserialize ) ]
1923struct StoreIndex {
@@ -130,11 +134,36 @@ impl ImageStore {
130134 let digest_hex = digest. strip_prefix ( "sha256:" ) . unwrap_or ( digest) ;
131135 let target_dir = self . store_dir . join ( "sha256" ) . join ( digest_hex) ;
132136
133- // Copy source to target if not already present
137+ // Copy source to target if not already present. Stage into a unique temp
138+ // dir then atomically rename into place, so a concurrent put() for the
139+ // same digest — or a copy that fails partway — can never leave a
140+ // half-populated content-addressed dir that a later caller mistakes for a
141+ // complete image (the bare check-then-copy raced on both counts).
134142 if !target_dir. exists ( ) {
135- copy_dir_recursive ( source_dir, & target_dir) . map_err ( |e| {
143+ let seq = PUT_SEQ . fetch_add ( 1 , Ordering :: Relaxed ) ;
144+ let staging = self . store_dir . join ( "sha256" ) . join ( format ! (
145+ ".staging-{}-{}-{}" ,
146+ digest_hex,
147+ std:: process:: id( ) ,
148+ seq
149+ ) ) ;
150+ let _ = std:: fs:: remove_dir_all ( & staging) ;
151+ copy_dir_recursive ( source_dir, & staging) . map_err ( |e| {
152+ let _ = std:: fs:: remove_dir_all ( & staging) ;
136153 BoxError :: OciImageError ( format ! ( "Failed to copy image to store: {}" , e) )
137154 } ) ?;
155+ if let Err ( e) = std:: fs:: rename ( & staging, & target_dir) {
156+ let _ = std:: fs:: remove_dir_all ( & staging) ;
157+ // A concurrent put() may have populated target_dir first (rename
158+ // onto a non-empty dir fails); that's fine — only propagate if the
159+ // image still isn't there.
160+ if !target_dir. exists ( ) {
161+ return Err ( BoxError :: OciImageError ( format ! (
162+ "Failed to publish image to store: {}" ,
163+ e
164+ ) ) ) ;
165+ }
166+ }
138167 }
139168
140169 let size_bytes = dir_size ( & target_dir) ;
0 commit comments