@@ -241,10 +241,32 @@ impl Display for Source {
241
241
write ! ( f, "{}+{}" , self . name, self . version)
242
242
}
243
243
}
244
+ #[ cfg( not( test) ) ]
244
245
fn registry_dir ( ) -> PathBuf {
245
246
forc_util:: user_forc_directory ( ) . join ( REG_DIR_NAME )
246
247
}
247
248
249
+ #[ cfg( test) ]
250
+ fn registry_dir ( ) -> PathBuf {
251
+ use once_cell:: sync:: Lazy ;
252
+ use std:: sync:: Mutex ;
253
+
254
+ static TEST_REGISTRY_DIR : Lazy < Mutex < Option < PathBuf > > > = Lazy :: new ( || Mutex :: new ( None ) ) ;
255
+
256
+ let mut dir = TEST_REGISTRY_DIR . lock ( ) . unwrap ( ) ;
257
+ if let Some ( ref path) = * dir {
258
+ path. clone ( )
259
+ } else {
260
+ let temp_dir = tempfile:: tempdir ( ) . expect ( "Failed to create temp dir for tests" ) ;
261
+ let path = temp_dir. path ( ) . join ( REG_DIR_NAME ) ;
262
+ std:: fs:: create_dir_all ( & path) . expect ( "Failed to create test registry dir" ) ;
263
+ // Keep the temp dir alive by leaking it (only for tests)
264
+ let leaked_path = temp_dir. keep ( ) . join ( REG_DIR_NAME ) ;
265
+ * dir = Some ( leaked_path. clone ( ) ) ;
266
+ leaked_path
267
+ }
268
+ }
269
+
248
270
fn registry_with_namespace_dir ( namespace : & Namespace ) -> PathBuf {
249
271
let base = registry_dir ( ) ;
250
272
match namespace {
@@ -456,10 +478,19 @@ async fn fetch(fetch_id: u64, pinned: &Pinned, ipfs_node: &IPFSNode) -> anyhow::
456
478
if path. exists ( ) {
457
479
let _ = fs:: remove_dir_all ( & path) ;
458
480
}
459
- fs:: create_dir_all ( & path) ?;
460
481
461
482
let cid = resolve_to_cid ( & index_file, pinned) ?;
462
483
484
+ // Create directory only after we've validated the package exists in the index
485
+ fs:: create_dir_all ( & path) ?;
486
+
487
+ // Use a cleanup guard to ensure directory is removed if fetch fails
488
+ let cleanup_guard = scopeguard:: guard ( & path, |path| {
489
+ if path. exists ( ) {
490
+ let _ = fs:: remove_dir_all ( path) ;
491
+ }
492
+ } ) ;
493
+
463
494
// Try IPFS first, fallback to CDN if it fails
464
495
let ipfs_result = match ipfs_node {
465
496
IPFSNode :: Local => {
@@ -476,11 +507,24 @@ async fn fetch(fetch_id: u64, pinned: &Pinned, ipfs_node: &IPFSNode) -> anyhow::
476
507
} ;
477
508
478
509
// If IPFS fails, try CDN fallback
479
- if let Err ( ipfs_error) = ipfs_result {
510
+ let fetch_result = if let Err ( ipfs_error) = ipfs_result {
480
511
println_action_green ( "Warning" , & format ! ( "IPFS fetch failed: {ipfs_error}" ) ) ;
481
512
fetch_from_s3 ( pinned, & path) . await . with_context ( || {
482
513
format ! ( "Both IPFS and CDN fallback failed. IPFS error: {ipfs_error}" )
483
- } ) ?;
514
+ } )
515
+ } else {
516
+ Ok ( ( ) )
517
+ } ;
518
+
519
+ match fetch_result {
520
+ Ok ( ( ) ) => {
521
+ // Fetch successful, defuse the cleanup guard so directory is preserved
522
+ scopeguard:: ScopeGuard :: into_inner ( cleanup_guard) ;
523
+ }
524
+ Err ( e) => {
525
+ // Fetch failed, cleanup guard will automatically remove the directory
526
+ return Err ( e) ;
527
+ }
484
528
}
485
529
486
530
Ok ( path)
@@ -637,12 +681,16 @@ where
637
681
638
682
#[ cfg( test) ]
639
683
mod tests {
640
- use super :: { file_location:: Namespace , resolve_to_cid, Pinned , Source } ;
684
+ use super :: {
685
+ block_on_any_runtime, fetch, file_location:: Namespace , registry_package_dir,
686
+ resolve_to_cid, Pinned , Source ,
687
+ } ;
641
688
use crate :: source:: {
642
689
ipfs:: Cid ,
643
690
reg:: index_file:: { IndexFile , PackageEntry } ,
691
+ IPFSNode ,
644
692
} ;
645
- use std:: str:: FromStr ;
693
+ use std:: { fs , str:: FromStr } ;
646
694
647
695
#[ test]
648
696
fn parse_pinned_entry_without_namespace ( ) {
@@ -792,4 +840,53 @@ mod tests {
792
840
|| error_msg. contains( "Other available versions: [1.0.0,1.1.0]" )
793
841
) ;
794
842
}
843
+
844
+ #[ test]
845
+ fn test_fetch_directory_cleanup_on_failure ( ) {
846
+ // The test itself doesn't need to assert anything about the result,
847
+ // the assertions inside the async block are what matter
848
+ block_on_any_runtime ( async {
849
+ let pinned = Pinned {
850
+ source : Source {
851
+ name : "nonexistent_test_package" . to_string ( ) ,
852
+ version : semver:: Version :: new ( 1 , 0 , 0 ) ,
853
+ namespace : Namespace :: Flat ,
854
+ } ,
855
+ // Valid CID format but this will fail because the package doesn't exist in the index
856
+ cid : Cid :: from_str ( "QmdMVqLqpba2mMB5AUjYCxubC6tLGevQFunpBkbC2UbrKS" ) . unwrap ( ) ,
857
+ } ;
858
+
859
+ // Get the expected package directory path
860
+ let expected_path = registry_package_dir (
861
+ & pinned. source . namespace ,
862
+ & pinned. source . name ,
863
+ & pinned. source . version ,
864
+ ) ;
865
+
866
+ // Ensure the directory doesn't exist initially
867
+ if expected_path. exists ( ) {
868
+ let _ = fs:: remove_dir_all ( & expected_path) ;
869
+ }
870
+ assert ! ( !expected_path. exists( ) ) ;
871
+
872
+ // Call the actual fetch function with an IPFS node that will fail
873
+ // This will fail during index lookup (the package doesn't exist in registry)
874
+ let fetch_id = 12345 ;
875
+ let ipfs_node = IPFSNode :: WithUrl ( "https://invalid-url.com" . to_string ( ) ) ;
876
+
877
+ let result = fetch ( fetch_id, & pinned, & ipfs_node) . await ;
878
+
879
+ // Verify that fetch failed (package not found in index)
880
+ assert ! ( result. is_err( ) ) ;
881
+ let error_msg = result. unwrap_err ( ) . to_string ( ) ;
882
+ assert ! ( error_msg. contains( "Failed to fetch nonexistent_test_package" ) ) ;
883
+
884
+ // Most importantly, verify that no directory was created or if it was created, it got cleaned up
885
+ assert ! (
886
+ !expected_path. exists( ) ,
887
+ "Directory should not exist after fetch failure, but it exists at: {}" ,
888
+ expected_path. display( )
889
+ ) ;
890
+ } ) ;
891
+ }
795
892
}
0 commit comments