-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathhttp.rs
More file actions
141 lines (124 loc) · 5.42 KB
/
http.rs
File metadata and controls
141 lines (124 loc) · 5.42 KB
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
use std::io::Write;
use anyhow::Context;
use log::info;
use super::BundleLoader;
/// A loader that reads bundles from an HTTP(S) server.
///
/// The server is expected to respond to either GET or POST requests with the bundle data as follows:
/// - If the `id` argument is present when calling `load`, then a GET/POST request with a parameter `id` is submitted to the server as follows:
/// `GET/POST <path-from-url>?id=<id>`
/// The server should respond with the bundle data if a bundle with the supplied ID is found or with an HTTP error status code
/// otherwise; or with another HTTP error status code if the request failed due to other problems (invalid auth, server error, etc.)
/// - If the `id` argument is not present when calling `load`, then a GET/POST request is submitted to the server as follows:
/// `GET/POST <path-from-url>`
/// The server should respond with the bundle data if a bundle is found or with an HTTP error status code otherwise; or with another
/// HTTP error status code if the request failed due to other problems (invalid auth, server error, etc.)
/// The server might also delete the provided bundle after it had been provided, in case the used request method is POST
///
/// In both cases (bundle loading with or without a bundle ID), the server should provide the bundle data in the response body
/// and the name of the bundle in the `Content-Disposition` header. If the `Content-Disposition` header is not present, then the name of the bundle
/// is assumed to be the ID of the bundle with the `.bundle` extension, or a random name with the `.bundle` extension if the ID is not present
#[derive(Debug, Clone)]
pub struct HttpLoader {
load_url: String,
auth: Option<String>,
use_post: bool,
id_as_bundle_file: bool,
#[allow(unused)]
logs_url: Option<String>,
}
impl HttpLoader {
/// Creates a new `HttpLoader`
///
/// # Arguments
/// - `load_url`: The URL of the server to load the bundles from
/// - `auth`: An optional authorization token to use when loading the bundles
/// If present, it will be used as the value of the `Authorization` header
/// in the request
/// - `use_post`: A flag indicating whether to fetch the bundle with a GET or a POST request
/// - `id_as_bundle_file`: A flag indicating whether to fetch the bundle with:
/// - `true`: A simple parameter-less GET/POST request of the form `<url>/<bundle-id>.bundle`
/// - `false`: With a GET/POST request with a parameter `<url>?id=<bundle-id>`
/// - `logs_url`: An optional URL of the server to check for uploaded logs;
/// if provided, the loader will only download a bundle if its logs are not yet uploaded, this preventing
/// flashing a bundle multiple times
pub const fn new(
load_url: String,
auth: Option<String>,
use_post: bool,
id_as_bundle_file: bool,
logs_url: Option<String>,
) -> Self {
Self {
load_url,
auth,
use_post,
id_as_bundle_file,
logs_url,
}
}
}
impl BundleLoader for HttpLoader {
async fn load<W>(&mut self, mut write: W, id: Option<&str>) -> anyhow::Result<String>
where
W: Write,
{
if let Some(id) = id {
info!(
"About to fetch a bundle with ID `{id}` from URL `{}`...",
self.load_url
);
} else {
info!("About to fetch a bundle from URL `{}`...", self.load_url);
}
let client = reqwest::Client::new();
let mut builder = if let Some(id) = id {
if self.id_as_bundle_file {
// When `id_as_bundle_file` is `true`, we only fetch `.bundle` bundles for now (though we can try out .bin and elf too)
let url = format!("{}/{id}.bundle", self.load_url.trim_end_matches('/'));
if self.use_post {
client.post(&url)
} else {
client.get(&url)
}
} else if self.use_post {
client.post(&self.load_url).query(&[("id", id)])
} else {
client.get(&self.load_url).query(&[("id", id)])
}
} else if self.use_post {
client.post(&self.load_url)
} else {
client.get(&self.load_url)
};
if let Some(auth) = self.auth.as_deref() {
builder = builder.header("Authorization", auth);
}
let response = builder.send().await.context("Request failed")?;
let mut response = response
.error_for_status()
.context("Request returned an error status")?;
let mut bundle_name = format!("{}.bundle", id.unwrap_or("firmware"));
if let Some(cont_disp) = response
.headers()
.get("Content-Disposition")
.and_then(|value| value.to_str().ok())
{
let mut split = cont_disp.split(";");
if let Some(name) = split.find_map(|part| part.trim().strip_prefix("filename=")) {
bundle_name = name.trim().to_string();
}
}
while let Some(bytes) = response
.chunk()
.await
.context("Reading the response failed")?
{
write
.write_all(&bytes)
.context("Loading the bundle failed")?;
}
info!("Loaded bundle `{}`", bundle_name);
Ok(bundle_name)
}
}