nd_util/
download_to_file.rs

1use anyhow::ensure;
2use anyhow::Context;
3use tokio::fs::File;
4use tokio::io::AsyncWriteExt;
5
6/// Download a url using a GET request to a tokio file.
7pub async fn download_to_file(
8    client: &reqwest::Client,
9    url: &str,
10    file: &mut File,
11) -> anyhow::Result<()> {
12    // Send the request
13    let mut response = client
14        .get(url)
15        .send()
16        .await
17        .context("failed to get headers")?
18        .error_for_status()?;
19
20    // Pre-allocate file space if possible.
21    let content_length = response.content_length();
22    if let Some(content_length) = content_length {
23        file.set_len(content_length)
24            .await
25            .context("failed to pre-allocate file")?;
26    }
27
28    // Keep track of the file size in case the server lies
29    let mut actual_length = 0;
30
31    // Download the file chunk-by-chunk
32    while let Some(chunk) = response.chunk().await.context("failed to get next chunk")? {
33        file.write_all(&chunk)
34            .await
35            .context("failed to write to file")?;
36
37        // This will panic if the server sends back a chunk larger than u64::MAX,
38        // which is incredibly unlikely/impossible.
39        actual_length += u64::try_from(chunk.len()).unwrap();
40    }
41
42    // Ensure file size matches content_length
43    if let Some(content_length) = content_length {
44        ensure!(
45            content_length == actual_length,
46            "content-length mismatch, {content_length} (content length) != {actual_length} (actual length)",
47        );
48    }
49
50    // Sync data
51    file.flush().await.context("failed to flush file")?;
52    file.sync_all().await.context("failed to sync file data")?;
53
54    Ok(())
55}
56
57#[cfg(test)]
58mod test {
59    use super::*;
60
61    #[tokio::test]
62    async fn it_works() {
63        let client = reqwest::Client::new();
64        tokio::fs::create_dir_all("test_tmp")
65            .await
66            .expect("failed to create tmp dir");
67        let mut file = File::create("test_tmp/download_to_file_google.html")
68            .await
69            .expect("failed to open");
70        download_to_file(&client, "http://google.com", &mut file)
71            .await
72            .expect("failed to download");
73    }
74}