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 } ;
@@ -30,6 +30,12 @@ pub struct DownloadCfg<'a> {
3030 pub process : & ' a Process ,
3131}
3232
33+ struct PartialDownload {
34+ path : PathBuf ,
35+ legacy_path : PathBuf ,
36+ existed : bool ,
37+ }
38+
3339impl < ' a > DownloadCfg < ' a > {
3440 /// construct a download configuration
3541 pub ( crate ) fn new ( cfg : & ' a Cfg < ' a > ) -> Self {
@@ -46,9 +52,9 @@ impl<'a> DownloadCfg<'a> {
4652 }
4753
4854 /// 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.
55+ /// Partial downloads are stored in `self.download_dir` under unique names.
56+ /// If the target file already exists, then the hash is checked and it is
57+ /// returned immediately without re-downloading.
5258 pub ( crate ) async fn download (
5359 & self ,
5460 url : & Url ,
@@ -58,40 +64,29 @@ impl<'a> DownloadCfg<'a> {
5864 utils:: ensure_dir_exists ( "Download Directory" , self . download_dir ) ?;
5965 let target_file = self . download_dir . join ( Path :: new ( hash) ) ;
6066
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- }
67+ if let Some ( file) = self . cached_file ( & target_file, hash) ? {
68+ debug ! ( url = url. as_ref( ) , "checksum passed" ) ;
69+ return Ok ( file) ;
7170 }
7271
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 ( ) ;
72+ let partial = Self :: partial_download ( & target_file) ?;
8373
8474 let mut hasher = Sha256 :: new ( ) ;
8575 let mut download = DownloadOptions :: try_from ( self . process ) ?
86- . start ( url, & partial_file_path )
76+ . start ( url, & partial . path )
8777 . with_hasher ( & mut hasher)
8878 . with_status ( status)
8979 . with_resume ( ) ;
9080
9181 if let Err ( e) = download. download ( ) . await {
9282 let is_network_failure = is_network_failure ( & e) ;
83+ if is_network_failure {
84+ Self :: keep_partial_for_resume ( & partial) ;
85+ } else {
86+ utils:: ensure_file_removed ( "partial download" , & partial. path ) ?;
87+ }
9388 let err = Err ( e) ;
94- return match ( partial_file_existed , is_network_failure) {
89+ return match ( partial . existed , is_network_failure) {
9590 ( true , true ) => err. context ( RustupError :: IncompletePartialFile ) ,
9691 ( true , false ) => err. context ( RustupError :: BrokenPartialFile ) ,
9792 ( false , _) => err,
@@ -102,8 +97,8 @@ impl<'a> DownloadCfg<'a> {
10297
10398 if hash != actual_hash {
10499 // Incorrect hash
105- if partial_file_existed {
106- self . clean ( & [ hash . to_string ( ) + ".partial" ] ) ? ;
100+ utils :: ensure_file_removed ( "partial download" , & partial . path ) ? ;
101+ if partial . existed {
107102 Err ( anyhow ! ( RustupError :: BrokenPartialFile ) )
108103 } else {
109104 Err ( RustupError :: ChecksumFailed {
@@ -115,13 +110,125 @@ impl<'a> DownloadCfg<'a> {
115110 }
116111 } else {
117112 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 } )
113+ self . finish_download ( & partial. path , & target_file, hash)
114+ }
115+ }
116+
117+ fn cached_file ( & self , target_file : & Path , hash : & str ) -> Result < Option < File > > {
118+ if target_file. exists ( ) {
119+ let cached_result = file_hash ( target_file) ?;
120+ if hash == cached_result {
121+ debug ! ( "reusing previously downloaded file" ) ;
122+ return Ok ( Some ( File {
123+ path : target_file. to_path_buf ( ) ,
124+ } ) ) ;
125+ } else {
126+ warn ! ( "bad checksum for cached download" ) ;
127+ fs:: remove_file ( target_file) . context ( "cleaning up previous download" ) ?;
128+ }
129+ }
130+
131+ Ok ( None )
132+ }
133+
134+ fn partial_download ( target_file : & Path ) -> Result < PartialDownload > {
135+ let legacy_path = Self :: legacy_partial_path ( target_file) ;
136+ let path = Self :: unique_partial_path ( target_file) ;
137+
138+ let existed = match fs:: rename ( & legacy_path, & path) {
139+ Ok ( ( ) ) => true ,
140+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => false ,
141+ Err ( e) => {
142+ return Err ( e) . with_context ( || {
143+ format ! (
144+ "claiming partial download '{}' for '{}'" ,
145+ legacy_path. display( ) ,
146+ path. display( )
147+ )
148+ } ) ;
149+ }
150+ } ;
151+
152+ Ok ( PartialDownload {
153+ path,
154+ legacy_path,
155+ existed,
156+ } )
157+ }
158+
159+ fn legacy_partial_path ( target_file : & Path ) -> PathBuf {
160+ target_file. with_file_name (
161+ target_file
162+ . file_name ( )
163+ . map ( |s| s. to_str ( ) . unwrap_or ( "_" ) )
164+ . unwrap_or ( "_" )
165+ . to_owned ( )
166+ + ".partial" ,
167+ )
168+ }
169+
170+ fn unique_partial_path ( target_file : & Path ) -> PathBuf {
171+ let file_name = target_file
172+ . file_name ( )
173+ . map ( |s| s. to_str ( ) . unwrap_or ( "_" ) )
174+ . unwrap_or ( "_" ) ;
175+ target_file. with_file_name ( format ! (
176+ "{file_name}.{}.partial" ,
177+ utils:: raw:: random_string( 16 )
178+ ) )
179+ }
180+
181+ fn keep_partial_for_resume ( partial : & PartialDownload ) {
182+ if !utils:: path_exists ( & partial. path ) {
183+ return ;
184+ }
185+
186+ if utils:: path_exists ( & partial. legacy_path ) {
187+ if let Err ( e) = utils:: ensure_file_removed ( "partial download" , & partial. path ) {
188+ warn ! (
189+ "could not remove duplicate partial download {} ({e})" ,
190+ partial. path. display( )
191+ ) ;
192+ }
193+ return ;
194+ }
195+
196+ if let Err ( e) = fs:: rename ( & partial. path , & partial. legacy_path ) {
197+ warn ! (
198+ "could not keep partial download {} for resumption at {} ({e})" ,
199+ partial. path. display( ) ,
200+ partial. legacy_path. display( )
201+ ) ;
202+ }
203+ }
204+
205+ fn finish_download (
206+ & self ,
207+ partial_file_path : & Path ,
208+ target_file : & Path ,
209+ hash : & str ,
210+ ) -> Result < File > {
211+ if let Some ( file) = self . cached_file ( target_file, hash) ? {
212+ utils:: ensure_file_removed ( "partial download" , partial_file_path) ?;
213+ return Ok ( file) ;
214+ }
215+
216+ match utils:: rename (
217+ "downloaded" ,
218+ partial_file_path,
219+ target_file,
220+ self . permit_copy_rename ,
221+ ) {
222+ Ok ( ( ) ) => Ok ( File {
223+ path : target_file. to_path_buf ( ) ,
224+ } ) ,
225+ Err ( e) => match self . cached_file ( target_file, hash) ? {
226+ Some ( file) => {
227+ utils:: ensure_file_removed ( "partial download" , partial_file_path) ?;
228+ Ok ( file)
229+ }
230+ None => Err ( e) ,
231+ } ,
125232 }
126233 }
127234
@@ -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