use crate::error::{CartonError, Result};
use dashmap::DashMap;
use futures::TryStreamExt;
use lazy_static::lazy_static;
use std::{pin::Pin, sync::Arc, task::Poll};
use tokio::io::{AsyncRead, AsyncSeek};
use tokio_util::compat::FuturesAsyncReadCompatExt;
use url::Url;
pub struct HTTPFile {
client: reqwest::Client,
url: String,
file_len: u64,
seek_pos: u64,
state: RequestState,
cached_data: Arc<CachedData>,
}
enum RequestState {
None,
#[cfg(target_family = "wasm")]
Request(Pin<Box<dyn std::future::Future<Output = FetchReturnType>>>),
#[cfg(not(target_family = "wasm"))]
Request(Pin<Box<dyn std::future::Future<Output = FetchReturnType> + Send + Sync>>),
#[cfg(target_family = "wasm")]
Response(Pin<Box<dyn AsyncRead>>),
#[cfg(not(target_family = "wasm"))]
Response(Pin<Box<dyn AsyncRead + Send + Sync>>),
}
lazy_static! {
static ref FILE_INFO_CACHE: DashMap<String, Arc<CachedData>> = DashMap::new();
static ref DL_URL_CACHE: DashMap<String, String> = DashMap::new();
}
struct CachedData {
file_len: u64,
file_end_data: Vec<u8>,
}
impl HTTPFile {
pub async fn new(
client: reqwest::Client,
url: String,
check_dl_header: bool,
) -> Result<HTTPFile> {
let mut head_res = None;
let url = if check_dl_header {
let mut parsed = Url::parse(&url).unwrap();
if parsed.host_str() == Some("carton.pub") {
parsed.set_host(Some("dl.carton.pub")).unwrap();
parsed.to_string()
} else {
if let Some(u) = DL_URL_CACHE.get(&url) {
u.clone()
} else {
let res = client.head(&url).send().await?;
let u = match res.headers().get("x-carton-dl-url") {
Some(v) => v.to_str().unwrap(),
None => {
head_res = Some(res);
&url
}
}
.to_owned();
DL_URL_CACHE.insert(url, u.clone());
u
}
}
} else {
url
};
let cached_data = match FILE_INFO_CACHE.get(&url) {
Some(len) => len.clone(),
None => {
let res = match head_res {
Some(v) => v,
None => client.head(&url).send().await?,
};
let file_len = res
.headers()
.get(reqwest::header::CONTENT_LENGTH)
.ok_or(CartonError::Other(
"Tried to fetch a URL that didn't have a content length",
))?
.to_str()
.map_err(|_| {
CartonError::Other("Tried to fetch a URL with an invalid content length.")
})?
.parse()
.map_err(|_| {
CartonError::Other("Tried to fetch a URL with an invalid content length.")
})?;
const NUM_END_BYTES: u64 = 66 * 1024;
let cached_data = Arc::new(CachedData {
file_len,
file_end_data: fetch_range(
&client,
&url,
file_len.saturating_sub(NUM_END_BYTES),
NUM_END_BYTES,
)
.await
.into(),
});
FILE_INFO_CACHE.insert(url.clone(), cached_data.clone());
cached_data
}
};
Ok(Self {
client,
url,
file_len: cached_data.file_len,
seek_pos: 0,
state: RequestState::None,
cached_data,
})
}
}
impl AsyncSeek for HTTPFile {
fn start_seek(mut self: Pin<&mut Self>, position: std::io::SeekFrom) -> std::io::Result<()> {
self.state = RequestState::None;
match position {
std::io::SeekFrom::Start(newpos) => {
self.seek_pos = newpos.min(self.file_len);
}
std::io::SeekFrom::End(offset) => {
self.seek_pos = self
.file_len
.saturating_add_signed(offset)
.min(self.file_len);
}
std::io::SeekFrom::Current(offset) => {
self.seek_pos = self
.seek_pos
.saturating_add_signed(offset)
.min(self.file_len);
}
}
Ok(())
}
fn poll_complete(
self: Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> Poll<std::io::Result<u64>> {
Poll::Ready(Ok(self.seek_pos))
}
}
impl AsyncRead for HTTPFile {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let range_start = self.seek_pos;
let cache_start_pos = self
.file_len
.saturating_sub(self.cached_data.file_end_data.len() as _);
if range_start >= cache_start_pos {
let cache_offset = (range_start - cache_start_pos) as _;
let num_bytes = self
.file_len
.saturating_sub(range_start)
.min(buf.remaining() as _);
self.seek_pos += num_bytes;
buf.put_slice(
&self.cached_data.file_end_data[cache_offset..(cache_offset + num_bytes as usize)],
);
return Poll::Ready(Ok(()));
}
loop {
match &mut self.state {
RequestState::None => {
let url = self.url.clone();
let client = self.client.clone();
let range_start = self.seek_pos;
if range_start == self.file_len {
return Poll::Ready(Ok(()));
}
self.state = RequestState::Request(Box::pin(async move {
fetch(&client, &url, range_start).await
}));
}
RequestState::Request(v) => match v.as_mut().poll(cx) {
Poll::Ready(res) => self.state = RequestState::Response(Box::pin(res)),
Poll::Pending => return Poll::Pending,
},
RequestState::Response(res) => {
let num_bytes_orig = buf.remaining();
let out = res.as_mut().poll_read(cx, buf);
let num_bytes_end = buf.remaining();
self.seek_pos += (num_bytes_orig - num_bytes_end) as u64;
return out;
}
}
}
}
}
async fn fetch_range(
client: &reqwest::Client,
url: &str,
range_start: u64,
num_bytes: u64,
) -> bytes::Bytes {
log::trace!("Request: {url} {range_start} {num_bytes}");
let range_end = range_start + num_bytes - 1;
let res = client
.get(url)
.header(
reqwest::header::RANGE,
format!("bytes={range_start}-{range_end}"),
)
.send()
.await
.unwrap();
if !res.status().is_success() {
panic!("Error fetching URL {}: {}", url, res.status());
}
res.bytes().await.unwrap()
}
#[cfg(not(target_family = "wasm"))]
type FetchReturnType = Box<dyn AsyncRead + Unpin + Send + Sync>;
#[cfg(target_family = "wasm")]
type FetchReturnType = Box<dyn AsyncRead + Unpin>;
async fn fetch(client: &reqwest::Client, url: &str, range_start: u64) -> FetchReturnType {
log::trace!("Request: {url} {range_start}");
let res = client
.get(url)
.header(reqwest::header::RANGE, format!("bytes={range_start}-"))
.send()
.await
.unwrap();
if !res.status().is_success() {
panic!("Error fetching URL {}: {}", url, res.status());
}
let stream = res
.bytes_stream()
.map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e))
.into_async_read();
let stream = stream.compat();
Box::new(stream)
}