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
18use crate::drivers::Request;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Downloader {
23 Curl,
25 Wget,
27 PowerShell,
29 Python3,
31 Tunnel,
33 Tcp,
35 OpenSSL,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Quiet {
42 Never,
44 Always,
46 OnSuccess,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ContentEncoding {
53 Gzip,
55}
56
57#[derive(Debug, Clone)]
58pub struct DownloadResult {
60 pub status_code: u16,
62 pub content_encoding: Option<ContentEncoding>,
64}
65
66#[derive(Debug, Clone)]
67pub struct RequestBuilder {
69 pub(crate) url: String,
70 pub(crate) headers: Vec<(String, String)>,
71 pub(crate) preferred: Vec<Downloader>,
72 pub(crate) follow_redirects: bool,
73 pub(crate) quiet: Quiet,
74}
75
76impl RequestBuilder {
77 pub fn new(url: impl Into<String>) -> Self {
79 Self {
80 url: url.into(),
81 headers: Vec::new(),
82 preferred: Vec::new(),
83 follow_redirects: true,
84 quiet: Quiet::Always,
85 }
86 }
87
88 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
90 self.headers.push((key.into(), value.into()));
91 self
92 }
93
94 pub fn preferred_downloader(mut self, preferred: Downloader) -> Self {
96 self.preferred.push(preferred);
97 self
98 }
99
100 pub fn follow_redirects(mut self, follow_redirects: bool) -> Self {
102 self.follow_redirects = follow_redirects;
103 self
104 }
105
106 pub fn quiet(mut self, quiet: Quiet) -> Self {
108 self.quiet = quiet;
109 self
110 }
111
112 pub fn fetch_string(self) -> Result<String, ResponseError> {
115 String::from_utf8(self.fetch_bytes()?)
116 .map_err(|e| ResponseError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
117 }
118
119 pub fn fetch_bytes(self) -> Result<Vec<u8>, ResponseError> {
122 url_parser::Url::new(&self.url)
123 .map_err(|e| ResponseError::Start(StartError::Url(e.to_string())))?;
124
125 let cancel = Arc::new(AtomicBool::new(false));
126 let buffer = Arc::new(Mutex::new(Vec::new()));
127 let memory_root = DownloadSink::buffer(buffer.clone());
128 let join = self
129 .start_first_backend(Arc::clone(&cancel), memory_root.clone())
130 .map_err(ResponseError::Start)?;
131
132 join.join().map_err(|_| ResponseError::ThreadPanicked)??;
133
134 Ok(std::mem::take(&mut *buffer.lock().unwrap()))
135 }
136
137 pub fn start(self, target_path: impl AsRef<Path>) -> Result<RequestHandle, StartError> {
139 let _ = url_parser::Url::new(&self.url).map_err(|e| StartError::Url(e.to_string()))?;
141
142 let target_file = OpenOptions::new()
143 .create(true)
144 .truncate(true)
145 .write(true)
146 .open(&target_path)?;
147
148 let cancel = Arc::new(AtomicBool::new(false));
149 let sink = DownloadSink::file(target_file);
150 let join = self.start_first_backend(cancel.clone(), sink)?;
151
152 Ok(RequestHandle {
153 cancel,
154 join: Some(join),
155 })
156 }
157
158 fn start_first_backend(
160 &self,
161 cancel: Arc<AtomicBool>,
162 sink: DownloadSink,
163 ) -> Result<JoinHandle<Result<DownloadResult, ResponseError>>, StartError> {
164 let mut saw_non_not_found: Option<io::Error> = None;
165 let mut saw_any_not_found = false;
166
167 let request = Request {
168 url: url_parser::Url::new(&self.url).map_err(|e| StartError::Url(e.to_string()))?,
169 headers: self.headers.clone(),
170 follow_redirects: self.follow_redirects,
171 quiet: self.quiet,
172 };
173
174 for d in candidate_downloaders(&self.preferred) {
175 match d
176 .driver()
177 .start(request.clone(), sink.clone(), Arc::clone(&cancel))
178 {
179 Ok(join) => return Ok(join),
180 Err(StartError::Url(msg)) => return Err(StartError::Url(msg)),
181 Err(StartError::NoDriverFound) => {
182 saw_any_not_found = true;
183 continue;
184 }
185 Err(StartError::IoError(e)) => {
186 if saw_non_not_found.is_none() {
187 saw_non_not_found = Some(e);
188 }
189 continue;
190 }
191 }
192 }
193
194 if let Some(e) = saw_non_not_found {
195 return Err(StartError::IoError(e));
196 }
197 if saw_any_not_found {
198 return Err(StartError::NoDriverFound);
199 }
200 Err(StartError::NoDriverFound)
201 }
202}
203
204impl Downloader {
205 pub(crate) fn driver(self) -> &'static dyn drivers::Driver {
206 match self {
207 Downloader::Curl => &drivers::curl::CurlDriver,
208 Downloader::Wget => &drivers::wget::WgetDriver,
209 Downloader::PowerShell => &drivers::powershell::PowerShellDriver,
210 Downloader::Python3 => &drivers::python3::Python3Driver,
211 Downloader::Tunnel => &drivers::tunnel::TunnelDriver,
212 Downloader::Tcp => &drivers::tunnel::TcpDriver,
213 Downloader::OpenSSL => &drivers::tunnel::OpenSslDriver,
214 }
215 }
216}
217
218#[derive(Debug)]
219pub struct RequestHandle {
221 cancel: Arc<AtomicBool>,
222 join: Option<JoinHandle<Result<DownloadResult, ResponseError>>>,
223}
224
225impl RequestHandle {
226 pub fn cancel(&self) {
228 self.cancel.store(true, Ordering::SeqCst);
229 }
230
231 pub fn join(mut self) -> Result<Response, ResponseError> {
233 let res = match self.join.take().expect("join called once").join() {
234 Ok(r) => r,
235 Err(_) => Err(ResponseError::ThreadPanicked),
236 }?;
237
238 Ok(Response {
239 status_code: res.status_code,
240 })
241 }
242}
243
244impl Drop for RequestHandle {
245 fn drop(&mut self) {
246 if self.join.is_some() {
247 self.cancel.store(true, Ordering::SeqCst);
248 }
250 }
251}
252
253#[derive(Debug, Clone)]
254pub struct Response {
256 pub status_code: u16,
258}
259
260#[derive(Debug)]
261pub enum StartError {
263 NoDriverFound,
265 IoError(io::Error),
267 Url(String),
269}
270
271impl From<io::Error> for StartError {
272 fn from(value: io::Error) -> Self {
273 Self::IoError(value)
274 }
275}
276
277#[derive(Debug)]
278pub enum ResponseError {
280 Io(io::Error),
282 InvalidUrl,
284 UnsupportedScheme,
286 Cancelled,
288 ThreadPanicked,
290 CommandFailed {
292 program: &'static str,
294 exit_code: Option<i32>,
296 stderr: String,
298 },
299 BadStatusCode(String),
301 GzipFailed {
303 exit_code: Option<i32>,
305 stderr: String,
307 },
308 Start(StartError),
310}
311
312impl From<io::Error> for ResponseError {
313 fn from(value: io::Error) -> Self {
314 Self::Io(value)
315 }
316}
317
318fn candidate_downloaders(preferred: &[Downloader]) -> Vec<Downloader> {
320 if !preferred.is_empty() {
321 return preferred.to_vec();
322 }
323 vec![
324 Downloader::Curl,
325 Downloader::Wget,
326 Downloader::PowerShell,
327 Downloader::Python3,
328 Downloader::Tunnel,
329 ]
330}