use std::{fmt::Debug, pin::Pin, task::Poll};
use crate::{
data_uri::{DataUriError, parse_data_uri},
request::{RequestError, get_request},
url_validation::UrlValidation,
};
use bytes::Bytes;
use futures::{Stream, TryStreamExt};
use mime::Mime;
use reqwest::{Url, header::CONTENT_TYPE};
use thiserror::Error;
use tracing::debug;
#[derive(Debug, Error)]
pub enum DownloadImageError {
#[error(transparent)]
Request(#[from] RequestError),
#[error(transparent)]
ResponseDownload(reqwest::Error),
#[error("content-type was missing or not an image mime type")]
InvalidMimeType,
#[error(transparent)]
DataUri(DataUriError),
}
pub enum ResolvedUri<'a> {
Data(&'a str),
Absolute(Url),
}
pub fn resolve_full_url<'a>(
base_url: &Url,
href: &'a str,
) -> Result<ResolvedUri<'a>, url::ParseError> {
if href.starts_with("data:") {
return Ok(ResolvedUri::Data(href));
}
let href = href.replace("&", "&");
let url = if href.starts_with("http") {
Url::parse(&href)
} else {
base_url.join(&href)
}?;
Ok(ResolvedUri::Absolute(url))
}
pub enum ImageStream {
Memory(Option<Bytes>),
Response(Pin<Box<dyn futures::Stream<Item = Result<Bytes, DownloadImageError>> + Send>>),
}
impl Debug for ImageStream {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageStream").finish()
}
}
impl Stream for ImageStream {
type Item = Result<Bytes, DownloadImageError>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
match self.get_mut() {
ImageStream::Memory(bytes) => {
let value = bytes.take();
Poll::Ready(value.map(Ok))
}
ImageStream::Response(stream) => stream.as_mut().poll_next(cx),
}
}
}
pub async fn download_image_href<D: UrlValidation>(
client: &reqwest::Client,
url: ResolvedUri<'_>,
) -> Result<(ImageStream, Mime), DownloadImageError> {
match url {
ResolvedUri::Data(data_uri) => parse_data_uri(data_uri)
.map_err(DownloadImageError::DataUri)
.and_then(|(bytes, mime)| {
if mime.type_() != mime::IMAGE {
return Err(DownloadImageError::InvalidMimeType);
}
Ok((ImageStream::Memory(Some(bytes)), mime))
}),
ResolvedUri::Absolute(url) => {
debug!(%url, "requesting remote image");
download_image::<D>(client, url).await
}
}
}
async fn download_image<D: UrlValidation>(
client: &reqwest::Client,
url: Url,
) -> Result<(ImageStream, Mime), DownloadImageError> {
let (response, _redirects) = get_request::<D>(client, url).await?;
let headers = response.headers();
let content_type = headers
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<Mime>().ok())
.ok_or(DownloadImageError::InvalidMimeType)?;
if content_type.type_() != mime::IMAGE {
return Err(DownloadImageError::InvalidMimeType);
}
let stream = response
.bytes_stream()
.map_err(DownloadImageError::ResponseDownload);
let stream = ImageStream::Response(Box::pin(stream));
Ok((stream, content_type))
}