11use std:: borrow:: Cow ;
22use std:: fs;
3- use std:: io:: Read ;
3+ use std:: io:: { self , Read } ;
44use std:: ops;
55use std:: path:: { Path , PathBuf } ;
66use std:: sync:: { Arc , Mutex } ;
@@ -46,9 +46,9 @@ impl<'a> DownloadCfg<'a> {
4646 }
4747
4848 /// Downloads a file and validates its hash. Resumes interrupted downloads.
49- /// Partial downloads are stored in `self.download_dir`, keyed by hash. If the
50- /// target file already exists, then the hash is checked and it is returned
51- /// immediately without re-downloading.
49+ /// Partial downloads are stored in `self.download_dir` under unique names.
50+ /// If the target file already exists, then the hash is checked and it is
51+ /// returned immediately without re-downloading.
5252 pub ( crate ) async fn download (
5353 & self ,
5454 url : & Url ,
@@ -58,40 +58,29 @@ impl<'a> DownloadCfg<'a> {
5858 utils:: ensure_dir_exists ( "Download Directory" , self . download_dir ) ?;
5959 let target_file = self . download_dir . join ( Path :: new ( hash) ) ;
6060
61- if target_file. exists ( ) {
62- let cached_result = file_hash ( & target_file) ?;
63- if hash == cached_result {
64- debug ! ( "reusing previously downloaded file" ) ;
65- debug ! ( url = url. as_ref( ) , "checksum passed" ) ;
66- return Ok ( File { path : target_file } ) ;
67- } else {
68- warn ! ( "bad checksum for cached download" ) ;
69- fs:: remove_file ( & target_file) . context ( "cleaning up previous download" ) ?;
70- }
61+ if let Some ( file) = self . cached_file ( & target_file, hash) ? {
62+ debug ! ( url = url. as_ref( ) , "checksum passed" ) ;
63+ return Ok ( file) ;
7164 }
7265
73- let partial_file_path = target_file. with_file_name (
74- target_file
75- . file_name ( )
76- . map ( |s| s. to_str ( ) . unwrap_or ( "_" ) )
77- . unwrap_or ( "_" )
78- . to_owned ( )
79- + ".partial" ,
80- ) ;
81-
82- let partial_file_existed = partial_file_path. exists ( ) ;
66+ let partial = Self :: partial_download ( & target_file) ?;
8367
8468 let mut hasher = Sha256 :: new ( ) ;
8569 let mut download = DownloadOptions :: try_from ( self . process ) ?
86- . start ( url, & partial_file_path )
70+ . start ( url, & partial . path )
8771 . with_hasher ( & mut hasher)
8872 . with_status ( status)
8973 . with_resume ( ) ;
9074
9175 if let Err ( e) = download. download ( ) . await {
9276 let is_network_failure = is_network_failure ( & e) ;
77+ if is_network_failure {
78+ Self :: keep_partial_for_resume ( & partial) ;
79+ } else {
80+ utils:: ensure_file_removed ( "partial download" , & partial. path ) ?;
81+ }
9382 let err = Err ( e) ;
94- return match ( partial_file_existed , is_network_failure) {
83+ return match ( partial . existed , is_network_failure) {
9584 ( true , true ) => err. context ( RustupError :: IncompletePartialFile ) ,
9685 ( true , false ) => err. context ( RustupError :: BrokenPartialFile ) ,
9786 ( false , _) => err,
@@ -102,8 +91,8 @@ impl<'a> DownloadCfg<'a> {
10291
10392 if hash != actual_hash {
10493 // Incorrect hash
105- if partial_file_existed {
106- self . clean ( & [ hash . to_string ( ) + ".partial" ] ) ? ;
94+ utils :: ensure_file_removed ( "partial download" , & partial . path ) ? ;
95+ if partial . existed {
10796 Err ( anyhow ! ( RustupError :: BrokenPartialFile ) )
10897 } else {
10998 Err ( RustupError :: ChecksumFailed {
@@ -115,13 +104,125 @@ impl<'a> DownloadCfg<'a> {
115104 }
116105 } else {
117106 debug ! ( url = url. as_ref( ) , "checksum passed" ) ;
118- utils:: rename (
119- "downloaded" ,
120- & partial_file_path,
121- & target_file,
122- self . permit_copy_rename ,
123- ) ?;
124- Ok ( File { path : target_file } )
107+ self . finish_download ( & partial. path , & target_file, hash)
108+ }
109+ }
110+
111+ fn cached_file ( & self , target_file : & Path , hash : & str ) -> Result < Option < File > > {
112+ if target_file. exists ( ) {
113+ let cached_result = file_hash ( target_file) ?;
114+ if hash == cached_result {
115+ debug ! ( "reusing previously downloaded file" ) ;
116+ return Ok ( Some ( File {
117+ path : target_file. to_path_buf ( ) ,
118+ } ) ) ;
119+ } else {
120+ warn ! ( "bad checksum for cached download" ) ;
121+ fs:: remove_file ( target_file) . context ( "cleaning up previous download" ) ?;
122+ }
123+ }
124+
125+ Ok ( None )
126+ }
127+
128+ fn partial_download ( target_file : & Path ) -> Result < PartialDownload > {
129+ let legacy_path = Self :: legacy_partial_path ( target_file) ;
130+ let path = Self :: unique_partial_path ( target_file) ;
131+
132+ let existed = match fs:: rename ( & legacy_path, & path) {
133+ Ok ( ( ) ) => true ,
134+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => false ,
135+ Err ( e) => {
136+ return Err ( e) . with_context ( || {
137+ format ! (
138+ "claiming partial download '{}' for '{}'" ,
139+ legacy_path. display( ) ,
140+ path. display( )
141+ )
142+ } ) ;
143+ }
144+ } ;
145+
146+ Ok ( PartialDownload {
147+ path,
148+ legacy_path,
149+ existed,
150+ } )
151+ }
152+
153+ fn legacy_partial_path ( target_file : & Path ) -> PathBuf {
154+ target_file. with_file_name (
155+ target_file
156+ . file_name ( )
157+ . map ( |s| s. to_str ( ) . unwrap_or ( "_" ) )
158+ . unwrap_or ( "_" )
159+ . to_owned ( )
160+ + ".partial" ,
161+ )
162+ }
163+
164+ fn unique_partial_path ( target_file : & Path ) -> PathBuf {
165+ let file_name = target_file
166+ . file_name ( )
167+ . map ( |s| s. to_str ( ) . unwrap_or ( "_" ) )
168+ . unwrap_or ( "_" ) ;
169+ target_file. with_file_name ( format ! (
170+ "{file_name}.{}.partial" ,
171+ utils:: raw:: random_string( 16 )
172+ ) )
173+ }
174+
175+ fn keep_partial_for_resume ( partial : & PartialDownload ) {
176+ if !utils:: path_exists ( & partial. path ) {
177+ return ;
178+ }
179+
180+ if utils:: path_exists ( & partial. legacy_path ) {
181+ if let Err ( e) = utils:: ensure_file_removed ( "partial download" , & partial. path ) {
182+ warn ! (
183+ "could not remove duplicate partial download {} ({e})" ,
184+ partial. path. display( )
185+ ) ;
186+ }
187+ return ;
188+ }
189+
190+ if let Err ( e) = fs:: rename ( & partial. path , & partial. legacy_path ) {
191+ warn ! (
192+ "could not keep partial download {} for resumption at {} ({e})" ,
193+ partial. path. display( ) ,
194+ partial. legacy_path. display( )
195+ ) ;
196+ }
197+ }
198+
199+ fn finish_download (
200+ & self ,
201+ partial_file_path : & Path ,
202+ target_file : & Path ,
203+ hash : & str ,
204+ ) -> Result < File > {
205+ if let Some ( file) = self . cached_file ( target_file, hash) ? {
206+ utils:: ensure_file_removed ( "partial download" , partial_file_path) ?;
207+ return Ok ( file) ;
208+ }
209+
210+ match utils:: rename (
211+ "downloaded" ,
212+ partial_file_path,
213+ target_file,
214+ self . permit_copy_rename ,
215+ ) {
216+ Ok ( ( ) ) => Ok ( File {
217+ path : target_file. to_path_buf ( ) ,
218+ } ) ,
219+ Err ( e) => match self . cached_file ( target_file, hash) ? {
220+ Some ( file) => {
221+ utils:: ensure_file_removed ( "partial download" , partial_file_path) ?;
222+ Ok ( file)
223+ }
224+ None => Err ( e) ,
225+ } ,
125226 }
126227 }
127228
@@ -458,6 +559,12 @@ fn file_hash(path: &Path) -> Result<String> {
458559 Ok ( faster_hex:: hex_string ( & hasher. finalize ( ) ) )
459560}
460561
562+ struct PartialDownload {
563+ path : PathBuf ,
564+ legacy_path : PathBuf ,
565+ existed : bool ,
566+ }
567+
461568pub ( crate ) struct File {
462569 path : PathBuf ,
463570}
@@ -469,3 +576,81 @@ impl ops::Deref for File {
469576 self . path . as_path ( )
470577 }
471578}
579+
580+ #[ cfg( test) ]
581+ mod tests {
582+ use std:: sync:: Arc ;
583+
584+ use sha2:: { Digest , Sha256 } ;
585+
586+ use super :: * ;
587+ use crate :: process:: TestProcess ;
588+
589+ #[ test]
590+ fn partial_download_claims_legacy_partial_for_resume ( ) {
591+ let tempdir = tempfile:: Builder :: new ( ) . prefix ( "rustup" ) . tempdir ( ) . unwrap ( ) ;
592+ let target_file = tempdir. path ( ) . join ( "abc123" ) ;
593+ let legacy_partial = DownloadCfg :: legacy_partial_path ( & target_file) ;
594+ fs:: write ( & legacy_partial, b"partial contents" ) . unwrap ( ) ;
595+
596+ let partial = DownloadCfg :: partial_download ( & target_file) . unwrap ( ) ;
597+
598+ assert ! ( partial. existed) ;
599+ assert_ne ! ( partial. path, legacy_partial) ;
600+ assert ! ( !legacy_partial. exists( ) ) ;
601+ assert_eq ! ( fs:: read( & partial. path) . unwrap( ) , b"partial contents" ) ;
602+ assert ! (
603+ partial
604+ . path
605+ . file_name( )
606+ . unwrap( )
607+ . to_str( )
608+ . unwrap( )
609+ . starts_with( "abc123." )
610+ ) ;
611+ assert ! (
612+ partial
613+ . path
614+ . file_name( )
615+ . unwrap( )
616+ . to_str( )
617+ . unwrap( )
618+ . ends_with( ".partial" )
619+ ) ;
620+ }
621+
622+ #[ test]
623+ fn finish_download_reuses_valid_cache_from_race ( ) {
624+ let tempdir = tempfile:: Builder :: new ( ) . prefix ( "rustup" ) . tempdir ( ) . unwrap ( ) ;
625+ let download_dir = tempdir. path ( ) . join ( "downloads" ) ;
626+ utils:: ensure_dir_exists ( "download dir" , & download_dir) . unwrap ( ) ;
627+
628+ let content = b"cached component contents" ;
629+ let hash = faster_hex:: hex_string ( & Sha256 :: digest ( content) ) ;
630+ let target_file = download_dir. join ( & hash) ;
631+ let partial_file = download_dir. join ( format ! ( "{hash}.other-process.partial" ) ) ;
632+ fs:: write ( & target_file, content) . unwrap ( ) ;
633+ fs:: write ( & partial_file, content) . unwrap ( ) ;
634+
635+ let tp = TestProcess :: default ( ) ;
636+ let tmp_cx = Arc :: new ( temp:: Context :: new (
637+ tempdir. path ( ) . join ( "tmp" ) ,
638+ DEFAULT_DIST_SERVER ,
639+ ) ) ;
640+ let cfg = DownloadCfg {
641+ tmp_cx,
642+ download_dir : & download_dir,
643+ tracker : DownloadTracker :: new ( false , & tp. process ) ,
644+ permit_copy_rename : tp. process . permit_copy_rename ( ) ,
645+ process : & tp. process ,
646+ } ;
647+
648+ let file = cfg
649+ . finish_download ( & partial_file, & target_file, & hash)
650+ . unwrap ( ) ;
651+
652+ assert_eq ! ( & * file, target_file. as_path( ) ) ;
653+ assert ! ( !partial_file. exists( ) ) ;
654+ assert_eq ! ( fs:: read( & target_file) . unwrap( ) , content) ;
655+ }
656+ }
0 commit comments