11//! Multipart upload handlers
22
33use crate :: { AppState , ApiError , S3ErrorCode } ;
4+ use crate :: pinning:: pin_for_user;
45use crate :: state:: UserSession ;
56use crate :: multipart:: UploadPart ;
67use crate :: xml;
@@ -10,7 +11,7 @@ use axum::{
1011 response:: { IntoResponse , Response } ,
1112} ;
1213use bytes:: Bytes ;
13- use fula_blockstore:: BlockStore ;
14+ use fula_blockstore:: { BlockStore , PinStore } ;
1415use fula_core:: metadata:: ObjectMetadata ;
1516use fula_crypto:: hashing:: md5_hash;
1617use serde:: Deserialize ;
@@ -139,6 +140,7 @@ pub async fn complete_multipart_upload(
139140 Extension ( session) : Extension < UserSession > ,
140141 Path ( ( bucket, key) ) : Path < ( String , String ) > ,
141142 Query ( params) : Query < MultipartParams > ,
143+ headers : HeaderMap ,
142144 _body : Bytes ,
143145) -> Result < Response , ApiError > {
144146 if !session. can_write ( ) {
@@ -167,10 +169,15 @@ pub async fn complete_multipart_upload(
167169 // Calculate total size
168170 let total_size: u64 = upload. parts . values ( ) . map ( |p| p. size ) . sum ( ) ;
169171
172+ // Collect all part CIDs for pinning
173+ let part_cids: Vec < cid:: Cid > = upload. parts . values ( )
174+ . filter_map ( |p| p. cid . parse ( ) . ok ( ) )
175+ . collect ( ) ;
176+
170177 // Create the final object metadata
171178 // In a real implementation, we'd create a DAG linking all parts
172- let first_part_cid: cid:: Cid = upload . parts . values ( ) . next ( )
173- . map ( |p| p . cid . parse ( ) . unwrap ( ) )
179+ let first_part_cid: cid:: Cid = part_cids . first ( )
180+ . copied ( )
174181 . ok_or_else ( || ApiError :: s3 ( S3ErrorCode :: InvalidPart , "No parts uploaded" ) ) ?;
175182
176183 let mut metadata = ObjectMetadata :: new ( first_part_cid, total_size, final_etag. clone ( ) )
@@ -187,7 +194,27 @@ pub async fn complete_multipart_upload(
187194 // Store in bucket
188195 let mut bucket_handle = state. bucket_manager . open_bucket ( & bucket) . await ?;
189196 bucket_handle. put_object ( key. clone ( ) , metadata) . await ?;
190- bucket_handle. flush ( ) . await ?;
197+ let bucket_root_cid = bucket_handle. flush ( ) . await ?;
198+
199+ // Pin the BUCKET ROOT CID to ensure tree structure survives GC.
200+ // This recursively pins all tree nodes AND all referenced object data (including parts).
201+ // NOTE: Pinning is async (fire-and-forget) to avoid blocking the response.
202+ {
203+ let block_store = Arc :: clone ( & state. block_store ) ;
204+ let pin_bucket = bucket. clone ( ) ;
205+ tokio:: spawn ( async move {
206+ let pin_name = format ! ( "bucket:{}" , pin_bucket) ;
207+ if let Err ( e) = block_store. pin ( & bucket_root_cid, Some ( & pin_name) ) . await {
208+ tracing:: warn!( cid = %bucket_root_cid, error = %e, "Failed to pin bucket root CID" ) ;
209+ } else {
210+ tracing:: info!( cid = %bucket_root_cid, bucket = %pin_name, "Bucket root CID pinned (recursive)" ) ;
211+ }
212+ } ) ;
213+ }
214+
215+ // Also pin to user's external pinning service if credentials provided
216+ // Pin the first part CID as the representative (or all parts)
217+ pin_for_user ( & headers, & first_part_cid, Some ( & key) ) . await ;
191218
192219 let location = format ! ( "/{}/{}" , bucket, key) ;
193220 let xml_response = xml:: complete_multipart_upload_result (
0 commit comments