1#![doc = include_str!("../README.md")]
2
3mod drivers;
4mod process;
5mod sink;
6mod url_parser;
7mod util;
8
9pub use sink::DownloadSink;
10
11use std::fs::OpenOptions;
12use std::io;
13use std::path::Path;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, Mutex};
16use std::thread::JoinHandle;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Downloader {
21 Curl,
23 Wget,
25 PowerShell,
27 Python3,
29 Tunnel,
31 Tcp,
33 OpenSSL,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Quiet {
40 Never,
42 Always,
44 OnSuccess,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ContentEncoding {
51 Gzip,
53}
54
55#[derive(Debug, Clone)]
56pub struct RequestBuilder {
58 pub(crate) url: String,
59 pub(crate) headers: Vec<(String, String)>,
60 pub(crate) preferred: Vec<Downloader>,
61 pub(crate) follow_redirects: bool,
62 pub(crate) quiet: Quiet,
63}
64
65#[derive(Debug, Clone)]
66pub struct DownloadResult {
68 pub status_code: u16,
70 pub content_encoding: Option<ContentEncoding>,
72}
73
74impl RequestBuilder {
75 pub fn new(url: impl Into<String>) -> Self {
77 Self {
78 url: url.into(),
79 headers: Vec::new(),
80 preferred: Vec::new(),
81 follow_redirects: true,
82 quiet: Quiet::Always,
83 }
84 }
85
86 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
88 self.headers.push((key.into(), value.into()));
89 self
90 }
91
92 pub fn preferred_downloader(mut self, preferred: Downloader) -> Self {
94 self.preferred.push(preferred);
95 self
96 }
97
98 pub fn follow_redirects(mut self, follow_redirects: bool) -> Self {
100 self.follow_redirects = follow_redirects;
101 self
102 }
103
104 pub fn quiet(mut self, quiet: Quiet) -> Self {
106 self.quiet = quiet;
107 self
108 }
109
110 pub fn fetch_string(self) -> Result<String, ResponseError> {
113 String::from_utf8(self.fetch_bytes()?)
114 .map_err(|e| ResponseError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
115 }
116
117 pub fn fetch_bytes(self) -> Result<Vec<u8>, ResponseError> {
120 url_parser::Url::new(&self.url)
121 .map_err(|e| ResponseError::Start(StartError::Url(e.to_string())))?;
122
123 let cancel = Arc::new(AtomicBool::new(false));
124 let buffer = Arc::new(Mutex::new(Vec::new()));
125 let memory_root = DownloadSink::buffer(buffer.clone());
126 let join = self
127 .start_first_backend(Arc::clone(&cancel), memory_root.clone())
128 .map_err(ResponseError::Start)?;
129
130 join.join().map_err(|_| ResponseError::ThreadPanicked)??;
131
132 Ok(std::mem::take(&mut *buffer.lock().unwrap()))
133 }
134
135 pub fn start(self, target_path: impl AsRef<Path>) -> Result<RequestHandle, StartError> {
137 let url = url_parser::Url::new(&self.url).map_err(|e| StartError::Url(e.to_string()))?;
139
140 let target_file = OpenOptions::new()
141 .create(true)
142 .truncate(true)
143 .write(true)
144 .open(&target_path)?;
145
146 let cancel = Arc::new(AtomicBool::new(false));
147 let sink = DownloadSink::file(target_file);
148 let join = self.start_first_backend(cancel.clone(), sink)?;
149
150 Ok(RequestHandle {
151 cancel,
152 join: Some(join),
153 })
154 }
155
156 fn start_first_backend(
158 &self,
159 cancel: Arc<AtomicBool>,
160 sink: DownloadSink,
161 ) -> Result<JoinHandle<Result<DownloadResult, ResponseError>>, StartError> {
162 let mut saw_non_not_found: Option<io::Error> = None;
163 let mut saw_any_not_found = false;
164
165 for d in candidate_downloaders(&self.preferred) {
166 match d
167 .driver()
168 .start(self.clone(), sink.clone(), Arc::clone(&cancel))
169 {
170 Ok(join) => return Ok(join),
171 Err(StartError::Url(msg)) => return Err(StartError::Url(msg)),
172 Err(StartError::NoDriverFound) => {
173 saw_any_not_found = true;
174 continue;
175 }
176 Err(StartError::IoError(e)) => {
177 if saw_non_not_found.is_none() {
178 saw_non_not_found = Some(e);
179 }
180 continue;
181 }
182 }
183 }
184
185 if let Some(e) = saw_non_not_found {
186 return Err(StartError::IoError(e));
187 }
188 if saw_any_not_found {
189 return Err(StartError::NoDriverFound);
190 }
191 Err(StartError::NoDriverFound)
192 }
193}
194
195impl Downloader {
196 pub(crate) fn driver(self) -> &'static dyn drivers::Driver {
197 match self {
198 Downloader::Curl => &drivers::curl::CurlDriver,
199 Downloader::Wget => &drivers::wget::WgetDriver,
200 Downloader::PowerShell => &drivers::powershell::PowerShellDriver,
201 Downloader::Python3 => &drivers::python3::Python3Driver,
202 Downloader::Tunnel => &drivers::tunnel::TunnelDriver,
203 Downloader::Tcp => &drivers::tunnel::TcpDriver,
204 Downloader::OpenSSL => &drivers::tunnel::OpenSslDriver,
205 }
206 }
207}
208
209#[derive(Debug)]
210pub struct RequestHandle {
212 cancel: Arc<AtomicBool>,
213 join: Option<JoinHandle<Result<DownloadResult, ResponseError>>>,
214}
215
216impl RequestHandle {
217 pub fn cancel(&self) {
219 self.cancel.store(true, Ordering::SeqCst);
220 }
221
222 pub fn join(mut self) -> Result<Response, ResponseError> {
224 let res = match self.join.take().expect("join called once").join() {
225 Ok(r) => r,
226 Err(_) => Err(ResponseError::ThreadPanicked),
227 }?;
228
229 Ok(Response {
230 status_code: res.status_code,
231 })
232 }
233}
234
235impl Drop for RequestHandle {
236 fn drop(&mut self) {
237 if self.join.is_some() {
238 self.cancel.store(true, Ordering::SeqCst);
239 }
241 }
242}
243
244#[derive(Debug, Clone)]
245pub struct Response {
247 pub status_code: u16,
249}
250
251#[derive(Debug)]
252pub enum StartError {
254 NoDriverFound,
256 IoError(io::Error),
258 Url(String),
260}
261
262impl From<io::Error> for StartError {
263 fn from(value: io::Error) -> Self {
264 Self::IoError(value)
265 }
266}
267
268#[derive(Debug)]
269pub enum ResponseError {
271 Io(io::Error),
273 InvalidUrl,
275 UnsupportedScheme,
277 Cancelled,
279 ThreadPanicked,
281 CommandFailed {
283 program: &'static str,
285 exit_code: Option<i32>,
287 stderr: String,
289 },
290 BadStatusCode(String),
292 GzipFailed {
294 exit_code: Option<i32>,
296 stderr: String,
298 },
299 Start(StartError),
301}
302
303impl From<io::Error> for ResponseError {
304 fn from(value: io::Error) -> Self {
305 Self::Io(value)
306 }
307}
308
309fn candidate_downloaders(preferred: &[Downloader]) -> Vec<Downloader> {
311 if !preferred.is_empty() {
312 return preferred.to_vec();
313 }
314 vec![
315 Downloader::Curl,
316 Downloader::Wget,
317 Downloader::PowerShell,
318 Downloader::Python3,
319 Downloader::Tunnel,
320 ]
321}