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
@@ -457,6 +558,12 @@ fn file_hash(path: &Path) -> Result<String> {
457558 Ok ( faster_hex:: hex_string ( & hasher. finalize ( ) ) )
458559}
459560
561+ struct PartialDownload {
562+ path : PathBuf ,
563+ legacy_path : PathBuf ,
564+ existed : bool ,
565+ }
566+
460567pub ( crate ) struct File {
461568 path : PathBuf ,
462569}
@@ -468,3 +575,81 @@ impl ops::Deref for File {
468575 self . path . as_path ( )
469576 }
470577}
578+
579+ #[ cfg( test) ]
580+ mod tests {
581+ use std:: sync:: Arc ;
582+
583+ use sha2:: { Digest , Sha256 } ;
584+
585+ use super :: * ;
586+ use crate :: process:: TestProcess ;
587+
588+ #[ test]
589+ fn partial_download_claims_legacy_partial_for_resume ( ) {
590+ let tempdir = tempfile:: Builder :: new ( ) . prefix ( "rustup" ) . tempdir ( ) . unwrap ( ) ;
591+ let target_file = tempdir. path ( ) . join ( "abc123" ) ;
592+ let legacy_partial = DownloadCfg :: legacy_partial_path ( & target_file) ;
593+ fs:: write ( & legacy_partial, b"partial contents" ) . unwrap ( ) ;
594+
595+ let partial = DownloadCfg :: partial_download ( & target_file) . unwrap ( ) ;
596+
597+ assert ! ( partial. existed) ;
598+ assert_ne ! ( partial. path, legacy_partial) ;
599+ assert ! ( !legacy_partial. exists( ) ) ;
600+ assert_eq ! ( fs:: read( & partial. path) . unwrap( ) , b"partial contents" ) ;
601+ assert ! (
602+ partial
603+ . path
604+ . file_name( )
605+ . unwrap( )
606+ . to_str( )
607+ . unwrap( )
608+ . starts_with( "abc123." )
609+ ) ;
610+ assert ! (
611+ partial
612+ . path
613+ . file_name( )
614+ . unwrap( )
615+ . to_str( )
616+ . unwrap( )
617+ . ends_with( ".partial" )
618+ ) ;
619+ }
620+
621+ #[ test]
622+ fn finish_download_reuses_valid_cache_from_race ( ) {
623+ let tempdir = tempfile:: Builder :: new ( ) . prefix ( "rustup" ) . tempdir ( ) . unwrap ( ) ;
624+ let download_dir = tempdir. path ( ) . join ( "downloads" ) ;
625+ utils:: ensure_dir_exists ( "download dir" , & download_dir) . unwrap ( ) ;
626+
627+ let content = b"cached component contents" ;
628+ let hash = faster_hex:: hex_string ( & Sha256 :: digest ( content) ) ;
629+ let target_file = download_dir. join ( & hash) ;
630+ let partial_file = download_dir. join ( format ! ( "{hash}.other-process.partial" ) ) ;
631+ fs:: write ( & target_file, content) . unwrap ( ) ;
632+ fs:: write ( & partial_file, content) . unwrap ( ) ;
633+
634+ let tp = TestProcess :: default ( ) ;
635+ let tmp_cx = Arc :: new ( temp:: Context :: new (
636+ tempdir. path ( ) . join ( "tmp" ) ,
637+ DEFAULT_DIST_SERVER ,
638+ ) ) ;
639+ let cfg = DownloadCfg {
640+ tmp_cx,
641+ download_dir : & download_dir,
642+ tracker : DownloadTracker :: new ( false , & tp. process ) ,
643+ permit_copy_rename : tp. process . permit_copy_rename ( ) ,
644+ process : & tp. process ,
645+ } ;
646+
647+ let file = cfg
648+ . finish_download ( & partial_file, & target_file, & hash)
649+ . unwrap ( ) ;
650+
651+ assert_eq ! ( & * file, target_file. as_path( ) ) ;
652+ assert ! ( !partial_file. exists( ) ) ;
653+ assert_eq ! ( fs:: read( & target_file) . unwrap( ) , content) ;
654+ }
655+ }
0 commit comments