@@ -240,6 +240,27 @@ async fn download_file_(
240240 res
241241}
242242
243+ pub ( crate ) async fn content_length ( url : & Url , process : & Process ) -> anyhow:: Result < Option < u64 > > {
244+ if url. scheme ( ) == "file" {
245+ let path = url
246+ . to_file_path ( )
247+ . map_err ( |_| anyhow:: anyhow!( "bogus file url: '{url}'" ) ) ?;
248+ return Ok ( Some ( std:: fs:: metadata ( path) ?. len ( ) ) ) ;
249+ }
250+
251+ let backend = select_backend ( process) ?;
252+ let timeout = timeout ( process) ?;
253+
254+ match backend {
255+ #[ cfg( feature = "curl-backend" ) ]
256+ Backend :: Curl => debug ! ( url = %url, "fetching content-length with curl" ) ,
257+ #[ cfg( any( feature = "reqwest-rustls-tls" , feature = "reqwest-native-tls" ) ) ]
258+ Backend :: Reqwest ( _) => debug ! ( url = %url, "fetching content-length with reqwest" ) ,
259+ } ;
260+
261+ backend. content_length ( url, timeout) . await
262+ }
263+
243264/// User agent header value for HTTP request.
244265/// See: https://github.com/rust-lang/rustup/issues/2860.
245266#[ cfg( feature = "curl-backend" ) ]
@@ -402,6 +423,15 @@ impl Backend {
402423 Self :: Reqwest ( tls) => tls. download ( url, resume_from, callback, timeout) . await ,
403424 }
404425 }
426+
427+ async fn content_length ( self , url : & Url , timeout : Duration ) -> anyhow:: Result < Option < u64 > > {
428+ match self {
429+ #[ cfg( feature = "curl-backend" ) ]
430+ Self :: Curl => curl:: content_length ( url, timeout) ,
431+ #[ cfg( any( feature = "reqwest-rustls-tls" , feature = "reqwest-native-tls" ) ) ]
432+ Self :: Reqwest ( tls) => tls. content_length ( url, timeout) . await ,
433+ }
434+ }
405435}
406436
407437#[ cfg( any( feature = "reqwest-rustls-tls" , feature = "reqwest-native-tls" ) ) ]
@@ -431,6 +461,17 @@ impl TlsBackend {
431461
432462 reqwest_be:: download ( url, resume_from, callback, client) . await
433463 }
464+
465+ async fn content_length ( self , url : & Url , timeout : Duration ) -> anyhow:: Result < Option < u64 > > {
466+ let client = match self {
467+ #[ cfg( feature = "reqwest-rustls-tls" ) ]
468+ Self :: Rustls => reqwest_be:: rustls_client ( timeout) ?,
469+ #[ cfg( feature = "reqwest-native-tls" ) ]
470+ Self :: NativeTls => reqwest_be:: native_tls_client ( timeout) ?,
471+ } ;
472+
473+ reqwest_be:: content_length ( url, client) . await
474+ }
434475}
435476
436477#[ derive( Debug , Copy , Clone ) ]
@@ -448,6 +489,7 @@ type DownloadCallback<'a> = &'a dyn Fn(Event<'_>) -> anyhow::Result<()>;
448489/// stack via libcurl
449490#[ cfg( feature = "curl-backend" ) ]
450491mod curl {
492+ use std:: cell:: Cell ;
451493 use std:: cell:: RefCell ;
452494 use std:: str;
453495 use std:: time:: Duration ;
@@ -458,6 +500,46 @@ mod curl {
458500
459501 use super :: { DownloadError , Event } ;
460502
503+ pub ( super ) fn content_length ( url : & Url , timeout : Duration ) -> Result < Option < u64 > > {
504+ let mut handle = Easy :: new ( ) ;
505+ handle. url ( url. as_ref ( ) ) ?;
506+ handle. follow_location ( true ) ?;
507+ handle. useragent ( super :: CURL_USER_AGENT ) ?;
508+ handle. nobody ( true ) ?;
509+ handle. connect_timeout ( timeout) ?;
510+
511+ let length = Cell :: new ( None ) ;
512+ {
513+ let mut transfer = handle. transfer ( ) ;
514+ transfer. header_function ( |header| {
515+ let Ok ( data) = str:: from_utf8 ( header) else {
516+ return true ;
517+ } ;
518+ let prefix = "content-length: " ;
519+ let Some ( ( dp, ds) ) = data. split_at_checked ( prefix. len ( ) ) else {
520+ return true ;
521+ } ;
522+ if !dp. eq_ignore_ascii_case ( prefix) {
523+ return true ;
524+ }
525+ if let Ok ( s) = ds. trim ( ) . parse :: < u64 > ( ) {
526+ length. set ( Some ( s) ) ;
527+ }
528+ true
529+ } ) ?;
530+
531+ transfer. perform ( ) ?;
532+ }
533+
534+ let code = handle. response_code ( ) ?;
535+ match code {
536+ 0 | 200 ..=299 => Ok ( length. get ( ) ) ,
537+ // Some servers do not support HEAD for file assets
538+ 405 => Ok ( None ) ,
539+ _ => Err ( DownloadError :: HttpStatus ( code) . into ( ) ) ,
540+ }
541+ }
542+
461543 pub ( super ) fn download (
462544 url : & Url ,
463545 resume_from : u64 ,
@@ -620,6 +702,22 @@ mod reqwest_be {
620702 Ok ( ( ) )
621703 }
622704
705+ pub ( super ) async fn content_length ( url : & Url , client : & Client ) -> anyhow:: Result < Option < u64 > > {
706+ let res = client
707+ . head ( url. as_str ( ) )
708+ . send ( )
709+ . await
710+ . context ( "error fetching content length" ) ?;
711+
712+ let status = res. status ( ) . into ( ) ;
713+ match status {
714+ 200 ..=299 => Ok ( res. content_length ( ) ) ,
715+ // Some servers do not support HEAD for file assets
716+ 405 => Ok ( None ) ,
717+ _ => Err ( DownloadError :: HttpStatus ( u32:: from ( status) ) . into ( ) ) ,
718+ }
719+ }
720+
623721 fn client_generic ( ) -> ClientBuilder {
624722 Client :: builder ( )
625723 // HACK: set `pool_max_idle_per_host` to `0` to avoid an issue in the underlying
0 commit comments