1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use anyhow::{anyhow, bail, Result};
use chrono::{DateTime, FixedOffset, Utc};
use futures_util::StreamExt;
use reqwest::header::{CONTENT_LENGTH, LAST_MODIFIED};
use std::path::Path;
use std::str::FromStr;
use tokio::io::AsyncWriteExt;
use crate::progress::Progress;
const S3_BUCKET: &str = "https://oxide-omicron-build.s3.amazonaws.com";
pub(crate) const BLOB: &str = "blob";
pub async fn download(progress: &impl Progress, source: &str, destination: &Path) -> Result<()> {
let blob = destination
.file_name()
.ok_or_else(|| anyhow!("missing blob filename"))?;
let url = format!("{}/{}", S3_BUCKET, source);
let client = reqwest::Client::new();
let head_response = client.head(&url).send().await?;
if !head_response.status().is_success() {
bail!("head failed! {:?}", head_response);
}
let headers = head_response.headers();
let content_length = headers
.get(CONTENT_LENGTH)
.ok_or_else(|| anyhow!("no content length on {} HEAD response!", url))?;
let content_length: u64 = u64::from_str(content_length.to_str()?)?;
if destination.exists() {
let last_modified = headers
.get(LAST_MODIFIED)
.ok_or_else(|| anyhow!("no last modified on {} HEAD response!", url))?;
let last_modified: DateTime<FixedOffset> =
chrono::DateTime::parse_from_rfc2822(last_modified.to_str()?)?;
let metadata = tokio::fs::metadata(&destination).await?;
let metadata_modified: DateTime<Utc> = metadata.modified()?.into();
if metadata.len() == content_length && metadata_modified == last_modified {
return Ok(());
}
}
let response = client.get(url).send().await?;
let last_modified = response
.headers()
.get(LAST_MODIFIED)
.ok_or_else(|| anyhow!("no last modified on GET response!"))?;
let last_modified: DateTime<FixedOffset> =
chrono::DateTime::parse_from_rfc2822(last_modified.to_str()?)?;
let mut file = tokio::fs::File::create(destination).await?;
let blob_progress = progress.sub_progress(content_length);
blob_progress.set_message(blob.to_string_lossy().into_owned().into());
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
blob_progress.increment(chunk.len() as u64);
}
drop(blob_progress);
file.sync_all().await?;
drop(file);
filetime::set_file_mtime(
destination,
filetime::FileTime::from_system_time(last_modified.into()),
)?;
Ok(())
}
#[test]
fn test_converts() {
let content_length = "1966080";
let last_modified = "Fri, 30 Apr 2021 22:37:39 GMT";
let content_length: u64 = u64::from_str(content_length).unwrap();
assert_eq!(1966080, content_length);
let _last_modified: DateTime<FixedOffset> =
chrono::DateTime::parse_from_rfc2822(last_modified).unwrap();
}