|
|
|
@ -5,7 +5,7 @@ use std::{path::Path, time::Duration}; |
|
|
|
|
|
|
|
use anyhow::anyhow;
|
|
|
|
use clap::{crate_name, crate_version};
|
|
|
|
use error::RequestError;
|
|
|
|
use error::{DownloadError, RequestError};
|
|
|
|
use futures_util::StreamExt as _;
|
|
|
|
use http::{
|
|
|
|
header::{CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, RANGE, USER_AGENT}, request::Builder as RequestBuilder, HeaderMap, HeaderValue, Request, Response, Uri
|
|
|
|
@ -30,6 +30,16 @@ type ClientResponseResult = Result<ClientResponse, RequestError>; |
|
|
|
type HttpClient = BoxCloneService<Request<Body>, ClientResponse, anyhow::Error>;
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub(super) enum DownloadState {
|
|
|
|
None,
|
|
|
|
GotHead,
|
|
|
|
#[allow(dead_code)]
|
|
|
|
Partial { content_type: Option<String>, size: usize },
|
|
|
|
#[allow(dead_code)]
|
|
|
|
Done { content_type: Option<String>, size: usize },
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub(super) struct Client {
|
|
|
|
client: HttpClient,
|
|
|
|
@ -37,6 +47,7 @@ pub(super) struct Client { |
|
|
|
body_timeout: Option<Duration>,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Client {
|
|
|
|
pub(super) fn new( buffer: usize |
|
|
|
, rate_limit: u64 |
|
|
|
@ -67,12 +78,12 @@ impl Client { |
|
|
|
Ok(Self {client, default_headers, body_timeout})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_body_timeout(mut self, timeout: Option<Duration>) -> Self {
|
|
|
|
pub(super) fn set_body_timeout(mut self, timeout: Option<Duration>) -> Self {
|
|
|
|
self.body_timeout = timeout;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_origin(mut self, origin: Option<String>) -> Self {
|
|
|
|
pub(super) fn set_origin(mut self, origin: Option<String>) -> Self {
|
|
|
|
if let Some(origin) = origin {
|
|
|
|
self.default_headers.insert(
|
|
|
|
ORIGIN,
|
|
|
|
@ -83,7 +94,7 @@ impl Client { |
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_user_agent(mut self, user_agent: Option<String>) -> Self {
|
|
|
|
pub(super) fn set_user_agent(mut self, user_agent: Option<String>) -> Self {
|
|
|
|
if let Some(user_agent) = user_agent {
|
|
|
|
self.default_headers.insert(
|
|
|
|
USER_AGENT,
|
|
|
|
@ -115,9 +126,7 @@ impl Client { |
|
|
|
, filename: impl AsRef<Path>
|
|
|
|
, uri: &Uri
|
|
|
|
, headers: &HeaderMap )
|
|
|
|
-> anyhow::Result<Option<(String, usize)>> {
|
|
|
|
|
|
|
|
let filename = filename.as_ref();
|
|
|
|
-> anyhow::Result<DownloadState> {
|
|
|
|
|
|
|
|
// - get all informations to eventually existing file
|
|
|
|
let mut from = util::file_size(&filename).await;
|
|
|
|
@ -129,10 +138,11 @@ impl Client { |
|
|
|
let content_type = util::get_header::<String>( response_headers
|
|
|
|
, CONTENT_TYPE )
|
|
|
|
. or(Some("unknown".into()));
|
|
|
|
let state = DownloadState::GotHead;
|
|
|
|
|
|
|
|
if let Some(content_length) = content_length {
|
|
|
|
if from != 0 && content_length - 1 <= from {
|
|
|
|
return Ok(None);
|
|
|
|
return Ok(state);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
from = 0;
|
|
|
|
@ -147,9 +157,9 @@ impl Client { |
|
|
|
// - open or create file
|
|
|
|
let file = util::open_or_create(&response.status(), &filename).await;
|
|
|
|
// - download Data
|
|
|
|
let size = self.clone().store_body(file, response.body_mut()).await?;
|
|
|
|
|
|
|
|
Ok(content_type.map(|c| (c, size)))
|
|
|
|
Ok( self.clone().store_body( file
|
|
|
|
, content_type
|
|
|
|
, response.body_mut() ).await? )
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn head( &mut self
|
|
|
|
@ -191,30 +201,36 @@ impl Client { |
|
|
|
|
|
|
|
async fn store_body( self
|
|
|
|
, mut file: File
|
|
|
|
, body: &mut ClientBody ) -> anyhow::Result<usize> {
|
|
|
|
, content_type: Option<String>
|
|
|
|
, body: &mut ClientBody ) -> Result<DownloadState, DownloadError> {
|
|
|
|
let mut body = BodyDataStream::new(body);
|
|
|
|
let mut written = 0;
|
|
|
|
let mut size = 0;
|
|
|
|
let mut state = DownloadState::Partial { content_type: content_type.clone(), size };
|
|
|
|
|
|
|
|
loop {
|
|
|
|
let data_future = body.next();
|
|
|
|
let data = if let Some(io_timeout) = self.body_timeout {
|
|
|
|
// give timeout somehow... probably from client.
|
|
|
|
timeout(io_timeout, data_future).await?
|
|
|
|
timeout(io_timeout, data_future).await
|
|
|
|
. map_err(|e| DownloadError::new(state.clone(), e.into()))?
|
|
|
|
} else {
|
|
|
|
data_future.await
|
|
|
|
};
|
|
|
|
|
|
|
|
match data {
|
|
|
|
None => break,
|
|
|
|
Some(Err(e)) => Err(anyhow!(e))?,
|
|
|
|
Some(Err(e)) => Err(DownloadError::new(state.clone(), anyhow!(e)))?,
|
|
|
|
Some(Ok(data)) => {
|
|
|
|
written += data.len();
|
|
|
|
file . write_all(&data).await?;
|
|
|
|
file . flush().await?;
|
|
|
|
size += data.len();
|
|
|
|
state = DownloadState::Partial { content_type: content_type.clone(), size };
|
|
|
|
file . write_all(&data).await
|
|
|
|
. map_err(|e| DownloadError::new(state.clone(), e.into()))?;
|
|
|
|
file . flush().await
|
|
|
|
. map_err(|e| DownloadError::new(state.clone(), e.into()))?;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(written)
|
|
|
|
Ok(DownloadState::Done { content_type, size })
|
|
|
|
}
|
|
|
|
}
|