use std::{fs, io};
#[derive(thiserror::Error, Debug)]
pub enum DownloadError {
#[error("HTTP error while downloading file: {0}")]
HttpError(#[from] reqwest::Error),
#[error("I/O error while downloading file: {0}")]
IoError(#[from] io::Error),
}
pub fn download_file(
uri: impl AsRef<str>,
destination: impl AsRef<std::path::Path>,
) -> Result<(), DownloadError> {
let client = reqwest::blocking::ClientBuilder::new()
.use_rustls_tls()
.build()?;
let mut response = client.get(uri.as_ref()).send()?.error_for_status()?;
let mut file = fs::File::create(destination.as_ref())?;
io::copy(&mut response, &mut file)?;
Ok(())
}
#[cfg(test)]
mod test {
use super::{DownloadError, download_file};
use indoc::indoc;
use reqwest::StatusCode;
use tempfile::NamedTempFile;
#[test]
fn test_self_signed_certificate() {
#[allow(unsafe_code)]
unsafe {
std::env::remove_var("SSL_CERT_FILE");
}
let temp_file = NamedTempFile::new().unwrap();
assert!(download_file("https://self-signed.badssl.com", temp_file.path()).is_err());
let badssl_self_signed_cert_dir = tempfile::tempdir().unwrap();
let badssl_self_signed_cert = badssl_self_signed_cert_dir
.path()
.join("badssl_self_signed_cert.pem");
std::fs::write(
&badssl_self_signed_cert,
indoc! { "
-----BEGIN CERTIFICATE-----
MIIDeTCCAmGgAwIBAgIJAMnA8BB8xT6wMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp
c2NvMQ8wDQYDVQQKDAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTAeFw0y
MTEwMTEyMDAzNTRaFw0yMzEwMTEyMDAzNTRaMGIxCzAJBgNVBAYTAlVTMRMwEQYD
VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ8wDQYDVQQK
DAZCYWRTU0wxFTATBgNVBAMMDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2
PmzAS2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMW
hyefdOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3A
xPxTuW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqve
ww9HdFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SY
QCeFxxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaMyMDAwCQYDVR0T
BAIwADAjBgNVHREEHDAaggwqLmJhZHNzbC5jb22CCmJhZHNzbC5jb20wDQYJKoZI
hvcNAQELBQADggEBAC4DensZ5tCTeCNJbHABYPwwqLUFOMITKOOgF3t8EqOan0CH
ST1NNi4jPslWrVhQ4Y3UbAhRBdqXl5N/NFfMzDosPpOjFgtifh8Z2s3w8vdlEZzf
A4mYTC8APgdpWyNgMsp8cdXQF7QOfdnqOfdnY+pfc8a8joObR7HEaeVxhJs+XL4E
CLByw5FR+svkYgCbQGWIgrM1cRpmXemt6Gf/XgFNP2PdubxqDEcnWlTMk8FCBVb1
nVDSiPjYShwnWsOOshshCRCAiIBPCKPX0QwKDComQlRrgMIvddaSzFFTKPoNZjC+
CUspSNnL7V9IIHvqKlRSmu+zIpm2VJCp1xLulk8=
-----END CERTIFICATE-----
"},
)
.unwrap();
#[allow(unsafe_code)]
unsafe {
std::env::set_var("SSL_CERT_FILE", badssl_self_signed_cert);
}
assert!(download_file("https://self-signed.badssl.com", temp_file.path()).is_ok());
#[allow(unsafe_code)]
unsafe {
std::env::remove_var("SSL_CERT_FILE");
}
}
#[test]
fn test_404() {
let temp_file = NamedTempFile::new().unwrap();
let result = download_file("https://www.google.com/404", temp_file.path());
match result {
Err(DownloadError::HttpError(error))
if error.is_status() && error.status() == Some(StatusCode::NOT_FOUND) => {}
result => panic!("Unexpected result: {result:?}"),
}
}
}