use anyhow::Error;
use futures_util::stream::StreamExt;
use reqwest::header;
use tokio::io::copy;
use tokio_util::io::StreamReader;
use crate::AsyncWriterFactory;
pub(crate) enum RetriableResult<R, E> {
Retriable(E),
Permanent(E),
Ok(R),
}
pub(crate) struct FetchMetadata {
pub content_type: String,
}
pub(crate) async fn get_url<AWF: AsyncWriterFactory>(
url: &str,
writer_factory: &mut AWF,
) -> RetriableResult<FetchMetadata, Error> {
let res = match reqwest::get(url)
.await
.and_then(|res| res.error_for_status())
{
Err(err) => {
if err.status().map(|s| s.is_client_error()).unwrap_or(false) {
return RetriableResult::Permanent(err.into());
} else {
return RetriableResult::Retriable(err.into());
}
}
Ok(res) => res,
};
let mut writer = match writer_factory.get_writer(res.content_length()).await {
Ok(w) => w,
Err(e) => return RetriableResult::Permanent(e),
};
let default_content_type = "application/binary";
let content_type = res
.headers()
.get(header::CONTENT_TYPE)
.map(|h| h.to_str().unwrap_or(default_content_type))
.unwrap_or(default_content_type)
.to_owned();
let stream = res
.bytes_stream()
.map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
let mut reader = StreamReader::new(stream);
match copy(&mut reader, &mut writer).await {
Ok(_) => {}
Err(e) => return RetriableResult::Retriable(e.into()),
};
return RetriableResult::Ok(FetchMetadata { content_type });
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_helpers::FakeDataServer;
use async_trait::async_trait;
use tokio::io::AsyncWrite;
#[derive(Default)]
struct SpyWriterFactory {
content_lengths: Vec<Option<u64>>,
buf: Vec<u8>,
}
#[async_trait]
impl AsyncWriterFactory for SpyWriterFactory {
async fn get_writer<'a>(
&'a mut self,
content_length: Option<u64>,
) -> anyhow::Result<Box<dyn AsyncWrite + Unpin + 'a>> {
self.content_lengths.push(content_length);
self.buf.clear();
Ok(Box::new(&mut self.buf))
}
}
#[tokio::test]
async fn get_writer_receives_content_length_on_success() {
let server = FakeDataServer::new(false, &[200]);
let mut factory = SpyWriterFactory::default();
match get_url(&server.data_url(), &mut factory).await {
RetriableResult::Ok(_) => {}
_ => panic!("expected a successful fetch"),
}
assert_eq!(factory.content_lengths, vec![Some(12)]);
assert_eq!(&factory.buf, b"hello, world");
}
#[tokio::test]
async fn get_writer_only_called_after_retry_success() {
let server = FakeDataServer::new(false, &[500, 200]);
let mut factory = SpyWriterFactory::default();
match get_url(&server.data_url(), &mut factory).await {
RetriableResult::Retriable(_) => {}
_ => panic!("expected a retriable failure"),
}
assert!(factory.content_lengths.is_empty());
assert!(factory.buf.is_empty());
match get_url(&server.data_url(), &mut factory).await {
RetriableResult::Ok(_) => {}
_ => panic!("expected a successful fetch"),
}
assert_eq!(factory.content_lengths, vec![Some(12)]);
assert_eq!(&factory.buf, b"hello, world");
}
#[tokio::test]
async fn get_writer_not_called_on_client_error() {
let server = FakeDataServer::new(false, &[400]);
let mut factory = SpyWriterFactory::default();
match get_url(&server.data_url(), &mut factory).await {
RetriableResult::Permanent(_) => {}
_ => panic!("expected a permanent failure"),
}
assert!(factory.content_lengths.is_empty());
}
}