nd_util/
download_to_file.rs1use anyhow::ensure;
2use anyhow::Context;
3use tokio::fs::File;
4use tokio::io::AsyncWriteExt;
5
6pub async fn download_to_file(
8 client: &reqwest::Client,
9 url: &str,
10 file: &mut File,
11) -> anyhow::Result<()> {
12 let mut response = client
14 .get(url)
15 .send()
16 .await
17 .context("failed to get headers")?
18 .error_for_status()?;
19
20 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 let mut actual_length = 0;
30
31 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 actual_length += u64::try_from(chunk.len()).unwrap();
40 }
41
42 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 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}