use std::{
io::{ErrorKind, SeekFrom},
path::{Path, PathBuf},
};
use async_fs::OpenOptions;
use futures_util::StreamExt;
use http_kit::{
BodyError, HttpError, StatusCode, header,
utils::{AsyncSeekExt, AsyncWriteExt},
};
use super::RequestBuilder;
#[derive(Debug, thiserror::Error)]
pub enum DownloadError<E: HttpError> {
#[error("request build error: {0}")]
Build(#[source] Box<crate::Error>),
#[error("request error: {0}")]
Remote(#[source] E),
#[error("failed to read response body: {0}")]
Body(#[source] BodyError),
#[error("file system error: {0}")]
Io(#[source] std::io::Error),
#[error("upstream returned unsuccessful status: {0}")]
Upstream(StatusCode),
}
impl<E: HttpError> HttpError for DownloadError<E> {
fn status(&self) -> StatusCode {
match self {
Self::Build(err) => err.status(),
Self::Remote(err) => err.status(),
Self::Body(_) => StatusCode::BAD_GATEWAY,
Self::Io(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Upstream(status) => *status,
}
}
}
impl<E> From<DownloadError<E>> for crate::Error
where
E: HttpError + Into<Self>,
{
fn from(err: DownloadError<E>) -> Self {
use crate::error::DownloadErrorKind;
match err {
DownloadError::Build(e) => *e,
DownloadError::Remote(e) => e.into(),
DownloadError::Body(e) => Self::Download(DownloadErrorKind::BodyRead(e.to_string())),
DownloadError::Io(e) => Self::Download(DownloadErrorKind::FileSystem(e)),
DownloadError::Upstream(status) => {
Self::Download(DownloadErrorKind::UpstreamError(status))
}
}
}
}
#[derive(Debug, Clone)]
pub struct DownloadReport {
pub path: PathBuf,
pub resumed_from: u64,
pub bytes_written: u64,
}
impl DownloadReport {
pub const fn total_bytes(&self) -> u64 {
self.resumed_from + self.bytes_written
}
}
#[derive(Debug, Clone, Copy)]
pub struct DownloadOptions {
pub resume_existing: bool,
}
impl Default for DownloadOptions {
fn default() -> Self {
Self {
resume_existing: true,
}
}
}
pub async fn download_to_path<T: crate::Client>(
mut builder: RequestBuilder<'_, T>,
path: impl AsRef<Path>,
options: DownloadOptions,
) -> Result<DownloadReport, DownloadError<T::Error>> {
let path_buf = path.as_ref().to_path_buf();
let existing_len = if options.resume_existing {
match async_fs::metadata(&path_buf).await {
Ok(meta) => meta.len(),
Err(err) if err.kind() == ErrorKind::NotFound => 0,
Err(err) => {
return Err(DownloadError::Io(err));
}
}
} else {
0
};
if existing_len > 0 {
let value = format!("bytes={existing_len}-");
builder = builder
.header(header::RANGE.as_str(), value)
.map_err(|error| DownloadError::Build(Box::new(error)))?;
}
let response = builder.await.map_err(DownloadError::Remote)?;
let status = response.status();
let mut body = response.into_body();
if !(status.is_success() || status == StatusCode::PARTIAL_CONTENT) {
return Err(DownloadError::Upstream(status));
}
let mut resumed_from = 0_u64;
let mut file = if existing_len > 0 && status == StatusCode::PARTIAL_CONTENT {
resumed_from = existing_len;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&path_buf)
.await
.map_err(DownloadError::Io)?;
file.seek(SeekFrom::Start(existing_len))
.await
.map_err(DownloadError::Io)?;
file
} else {
OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path_buf)
.await
.map_err(DownloadError::Io)?
};
let mut bytes_written = 0_u64;
while let Some(chunk) = body.next().await {
let chunk = chunk.map_err(DownloadError::Body)?;
file.write_all(&chunk).await.map_err(DownloadError::Io)?;
bytes_written += chunk.len() as u64;
}
file.flush().await.map_err(DownloadError::Io)?;
Ok(DownloadReport {
path: path_buf,
resumed_from,
bytes_written,
})
}