@@ -270,6 +270,8 @@ pub enum EphemeralExe {
270
270
version : EphemeralExeVersion ,
271
271
/// Destination directory or the user temp directory if none set.
272
272
dest_dir : Option < String > ,
273
+ /// How long to cache the download for. None means forever.
274
+ ttl : Option < Duration > ,
273
275
} ,
274
276
}
275
277
@@ -309,7 +311,11 @@ impl EphemeralExe {
309
311
}
310
312
Ok ( path)
311
313
}
312
- EphemeralExe :: CachedDownload { version, dest_dir } => {
314
+ EphemeralExe :: CachedDownload {
315
+ version,
316
+ dest_dir,
317
+ ttl,
318
+ } => {
313
319
let dest_dir = dest_dir
314
320
. as_ref ( )
315
321
. map ( PathBuf :: from)
@@ -334,8 +340,7 @@ impl EphemeralExe {
334
340
dest. display( )
335
341
) ;
336
342
337
- // If it already exists, skip
338
- if dest. exists ( ) {
343
+ if dest. exists ( ) && remove_file_past_ttl ( ttl, & dest) ? {
339
344
return Ok ( dest) ;
340
345
}
341
346
@@ -379,6 +384,7 @@ impl EphemeralExe {
379
384
& info. archive_url ,
380
385
Path :: new ( & info. file_to_extract ) ,
381
386
& dest,
387
+ false ,
382
388
)
383
389
. await ?
384
390
{
@@ -433,7 +439,7 @@ fn get_free_port(bind_ip: &str) -> io::Result<u16> {
433
439
let ( socket, _addr) = listen. accept ( ) ?;
434
440
435
441
// Explicitly drop the socket to close the connection from the listening side first
436
- std :: mem :: drop ( socket) ;
442
+ drop ( socket) ;
437
443
}
438
444
439
445
Ok ( addr. port ( ) )
@@ -447,6 +453,7 @@ async fn lazy_download_exe(
447
453
uri : & str ,
448
454
file_to_extract : & Path ,
449
455
dest : & Path ,
456
+ already_tried_cleaning_old : bool ,
450
457
) -> anyhow:: Result < bool > {
451
458
// If it already exists, do not extract
452
459
if dest. exists ( ) {
@@ -474,7 +481,16 @@ async fn lazy_download_exe(
474
481
// This match only gets Ok if the file was downloaded and extracted to the
475
482
// temporary path
476
483
match file {
477
- Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: AlreadyExists => {
484
+ Err ( err) if err. kind ( ) == io:: ErrorKind :: AlreadyExists => {
485
+ // If the download lock file exists but is old, delete it and try again, since it may
486
+ // have been left by an abandoned process.
487
+ if !already_tried_cleaning_old
488
+ && temp_dest. metadata ( ) ?. modified ( ) ?. elapsed ( ) ?. as_secs ( ) > 90
489
+ {
490
+ std:: fs:: remove_file ( temp_dest) ?;
491
+ return Box :: pin ( lazy_download_exe ( client, uri, file_to_extract, dest, true ) ) . await ;
492
+ }
493
+
478
494
// Since it already exists, we'll try once a second for 20 seconds
479
495
// to wait for it to be done, then return false so the caller can
480
496
// try again.
@@ -530,7 +546,7 @@ async fn download_and_extract(
530
546
// We have to map the error type to an io error
531
547
let stream = resp
532
548
. bytes_stream ( )
533
- . map ( |item| item. map_err ( |err| std :: io:: Error :: new ( std :: io:: ErrorKind :: Other , err) ) ) ;
549
+ . map ( |item| item. map_err ( |err| io:: Error :: new ( io:: ErrorKind :: Other , err) ) ) ;
534
550
535
551
// Since our tar/zip impls use sync IO, we have to create a bridge and run
536
552
// in a blocking closure.
@@ -574,12 +590,36 @@ async fn download_and_extract(
574
590
. await ?
575
591
}
576
592
593
+ /// Remove the file if it's older than the TTL. Returns true if the current file can be re-used,
594
+ /// returns false if it was removed or should otherwise be re-downloaded.
595
+ fn remove_file_past_ttl ( ttl : & Option < Duration > , dest : & PathBuf ) -> Result < bool , anyhow:: Error > {
596
+ match ttl {
597
+ None => return Ok ( true ) ,
598
+ Some ( ttl) => {
599
+ if let Ok ( mtime) = dest. metadata ( ) . and_then ( |d| d. modified ( ) ) {
600
+ if mtime. elapsed ( ) . unwrap_or_default ( ) . lt ( ttl) {
601
+ return Ok ( true ) ;
602
+ } else {
603
+ // Remove so we can re-download
604
+ std:: fs:: remove_file ( dest) ?;
605
+ }
606
+ }
607
+ // If we couldn't read the mtime something weird is probably up, so
608
+ // re-download
609
+ }
610
+ }
611
+ Ok ( false )
612
+ }
613
+
577
614
#[ cfg( test) ]
578
615
mod tests {
579
- use super :: get_free_port;
616
+ use super :: { get_free_port, remove_file_past_ttl } ;
580
617
use std:: {
581
618
collections:: HashSet ,
619
+ env:: temp_dir,
620
+ fs:: File ,
582
621
net:: { TcpListener , TcpStream } ,
622
+ time:: { Duration , SystemTime } ,
583
623
} ;
584
624
585
625
#[ test]
@@ -609,6 +649,22 @@ mod tests {
609
649
}
610
650
}
611
651
652
+ #[ tokio:: test]
653
+ async fn respects_file_ttl ( ) {
654
+ let rand_fname = format ! ( "{}" , rand:: random:: <u64 >( ) ) ;
655
+ let temp_dir = temp_dir ( ) ;
656
+
657
+ let dest_file_path = temp_dir. join ( format ! ( "core-test-{}" , & rand_fname) ) ;
658
+ let dest_file = File :: create ( & dest_file_path) . unwrap ( ) ;
659
+ let set_time_to = SystemTime :: now ( ) - Duration :: from_secs ( 100 ) ;
660
+ dest_file. set_modified ( set_time_to) . unwrap ( ) ;
661
+
662
+ remove_file_past_ttl ( & Some ( Duration :: from_secs ( 60 ) ) , & dest_file_path) . unwrap ( ) ;
663
+
664
+ // file should be gone
665
+ assert ! ( !dest_file_path. exists( ) ) ;
666
+ }
667
+
612
668
fn try_listen_and_dial_on ( host : & str , port : u16 ) -> std:: io:: Result < ( ) > {
613
669
let listener = TcpListener :: bind ( ( host, port) ) ?;
614
670
let _stream = TcpStream :: connect ( ( host, port) ) ?;
@@ -617,7 +673,7 @@ mod tests {
617
673
let ( socket, _addr) = listener. accept ( ) ?;
618
674
619
675
// Explicitly drop the socket to close the connection from the listening side first
620
- std :: mem :: drop ( socket) ;
676
+ drop ( socket) ;
621
677
622
678
Ok ( ( ) )
623
679
}
0 commit comments