use crate::{bytes::BytesStream, entry_data::EntryData, reader::Reader};
use actix_web::{
HttpRequest, HttpResponse,
body::{BodySize, BoxBody, MessageBody},
http::{
StatusCode,
header::{ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, RANGE},
},
};
use bytes::Bytes;
use futures_core::Stream;
use http_range_header::parse_range_header;
use pin_project::{pin_project, pinned_drop};
use std::{
pin::Pin,
task::{Context, Poll, ready},
};
use tokio::sync::oneshot;
impl<D: EntryData> Reader<D> {
pub fn into_responder(self) -> ActixWebAdapter<D> {
ActixWebAdapter {
inner: Inner {
bytes_stream: self.into_bytes_stream(),
drop_channel: None,
},
}
}
pub fn into_responder_with_channel(self) -> (ActixWebAdapter<D>, oneshot::Receiver<Reader<D>>) {
let (sender, receiver) = oneshot::channel();
let adapter = ActixWebAdapter {
inner: Inner {
bytes_stream: self.into_bytes_stream(),
drop_channel: Some(sender),
},
};
(adapter, receiver)
}
}
pub struct ActixWebAdapter<D: EntryData + 'static> {
inner: Inner<D>,
}
impl<D: EntryData + 'static> actix_web::Responder for ActixWebAdapter<D> {
type Body = BoxBody;
fn respond_to(mut self, req: &HttpRequest) -> HttpResponse<Self::Body> {
let mut response = HttpResponse::Ok();
let size = self.inner.bytes_stream.reader_ref().size();
response.insert_header((CONTENT_TYPE, "application/zip"));
response.insert_header((ACCEPT_RANGES, "bytes"));
let (offset, length) = if let Some(ranges_header) = req.headers().get(RANGE) {
let Some(parsed) = ranges_header
.to_str()
.ok()
.and_then(|ranges_str| parse_range_header(ranges_str).ok())
else {
return response.status(StatusCode::BAD_REQUEST).finish();
};
let Some(validated) = parsed.validate(size).ok() else {
response.insert_header((CONTENT_RANGE, format!("bytes */{size}")));
return response.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
};
let range = validated
.first()
.expect("Range header must be nonempty, this is verified by the parser");
response.insert_header((
CONTENT_RANGE,
format!("bytes {}-{}/{}", *range.start(), *range.end(), size),
));
(*range.start(), *range.end() - *range.start() + 1)
} else {
(0, size)
};
response.insert_header((CONTENT_LENGTH, length));
if offset != 0 || length != size {
response.status(StatusCode::PARTIAL_CONTENT);
}
self.inner
.bytes_stream
.reader_mut()
.seek_from_start_mut(offset);
response.body(Body::new(self.inner, length))
}
}
#[pin_project(PinnedDrop)]
struct Inner<D>
where
D: EntryData + 'static,
{
#[pin]
bytes_stream: BytesStream<D>,
drop_channel: Option<oneshot::Sender<Reader<D>>>,
}
#[pinned_drop]
impl<D> PinnedDrop for Inner<D>
where
D: EntryData + 'static,
{
fn drop(self: Pin<&mut Self>) {
let projected = self.project();
let Some(drop_channel) = projected.drop_channel.take() else {
return;
};
let extracted = projected.bytes_stream.reader_pin_mut().take_pinned();
let _ = drop_channel.send(extracted);
}
}
#[pin_project]
pub struct Body<D: EntryData + 'static> {
#[pin]
inner: Inner<D>,
size: u64,
remaining: u64,
}
impl<D: EntryData + 'static> Body<D> {
fn new(inner: Inner<D>, size: u64) -> Self {
Body {
inner,
size,
remaining: size,
}
}
}
impl<D: EntryData + 'static> MessageBody for Body<D> {
type Error = std::io::Error;
fn size(&self) -> BodySize {
BodySize::Sized(self.size)
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let projected = self.project();
if *projected.remaining == 0 {
return Poll::Ready(None);
}
let bytes_stream = projected.inner.project().bytes_stream;
let block = ready!(bytes_stream.poll_next(cx)).expect("The total number of bytes written is limited, we should never reach the actual end of the stream")?;
if *projected.remaining < (block.len() as u64) {
let limited_len =
usize::try_from(*projected.remaining).expect("In this branch remaining is small");
*projected.remaining = 0;
Poll::Ready(Some(Ok(block.slice(0..limited_len))))
} else {
*projected.remaining -= block.len() as u64;
Poll::Ready(Some(Ok(block)))
}
}
}
#[cfg(test)]
mod test {
use actix_web::{Responder, body::MessageBody as _, http::header::RANGE};
use assert2::assert;
use futures_util::{StreamExt, stream::poll_fn};
use std::pin::pin;
use test_strategy::proptest;
use tokio::sync::oneshot;
use crate::{Builder, BytesStream, Reader, test_util::skip_length};
use super::{Body, Inner};
#[tokio::test]
async fn default_response_headers() {
let reader = Builder::<&[u8]>::new().build();
let size = reader.size();
let responder = reader.into_responder();
let req = actix_web::test::TestRequest::get().to_http_request();
let response = responder.respond_to(&req);
let headers = response.headers();
assert!(response.status().as_u16() == 200);
let accept_ranges = headers
.get("Accept-Ranges")
.expect("Response must contain Accept-Ranges");
assert!(accept_ranges == "bytes");
let content_length = headers
.get("Content-Length")
.expect("Response must contain Content-Length");
assert!(content_length == format!("{size}").as_str());
assert!(headers.get("Content-Range").is_none());
}
#[tokio::test]
async fn range_response_headers() {
let reader = Builder::<&[u8]>::new().build();
let size = reader.size();
let responder = reader.into_responder();
let req = actix_web::test::TestRequest::get()
.insert_header((RANGE, "bytes=0-5"))
.to_http_request();
let response = responder.respond_to(&req);
let headers = response.headers();
assert!(response.status().as_u16() == 206);
let accept_ranges = headers
.get("Accept-Ranges")
.expect("Response must contain Accept-Ranges");
assert!(accept_ranges == "bytes");
let content_length = headers
.get("Content-Length")
.expect("Response must contain Content-Length");
assert!(content_length == "6");
let content_range = headers
.get("Content-Range")
.expect("Response must contain Content-Range");
assert!(content_range == format!("bytes 0-5/{size}").as_str());
}
#[proptest(async = "tokio")]
async fn reader_and_callback_spawns_callback(reader: Reader<Vec<u8>>) {
let size = reader.size();
let (tx, mut rx) = oneshot::channel();
let bsac = Inner {
bytes_stream: BytesStream::new(reader),
drop_channel: Some(tx),
};
assert!(rx.try_recv().is_err());
drop(bsac);
let remaining_reader = rx.await.unwrap();
assert!(remaining_reader.size() == size);
}
#[proptest(async = "tokio")]
async fn body_size_limiting(reader: Reader<Vec<u8>>, #[strategy(0f64..=1f64)] size_f: f64) {
let read_length = skip_length(usize::try_from(reader.size()).unwrap(), size_f);
let mut body = pin!(Body::new(reader.into_responder().inner, read_length));
let data = poll_fn(|cx| body.as_mut().poll_next(cx))
.fold(Vec::new(), |mut accumulator, block| async {
accumulator.extend_from_slice(block.unwrap().as_ref());
accumulator
})
.await;
assert!(data.len() as u64 == read_length);
}
}