use std::io;
use std::num::NonZeroUsize;
use std::path::Path;
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::Bytes;
use http::HeaderMap;
use http_body::{Body, Frame, SizeHint};
use primitives_rs::http::file_bodies::{FileBody, FileBodyConfig};
use primitives_rs::http::media_types::find_media_type_by_extension;
use primitives_rs::http::range_requests::{
ByteRange, RangeLimits, RangeRepresentation, RangeSelection, select_byte_ranges,
};
use tokio::fs::File;
const MAX_FRAME_SIZE: NonZeroUsize = NonZeroUsize::new(64 * 1024).unwrap();
const UNKNOWN_CONTENT_TYPE: &str = "application/octet-stream";
#[derive(Debug)]
pub struct StaticPublicAssetBody {
state: AssetBodyState,
}
#[derive(Debug)]
enum AssetBodyState {
Buffered(Bytes),
File(FileBody),
LengthOnly(u64),
Ended,
}
impl StaticPublicAssetBody {
pub(crate) async fn from_file(file: File, range: FileRange) -> io::Result<Self> {
let body = FileBody::from_file(
file,
FileBodyConfig {
start: range.start,
length: range.length,
max_frame_size: MAX_FRAME_SIZE,
},
)
.await?;
Ok(Self {
state: AssetBodyState::File(body),
})
}
pub(crate) fn from_length_only(length: u64) -> Self {
Self {
state: AssetBodyState::LengthOnly(length),
}
}
pub(crate) fn from_bytes(bytes: Bytes) -> Self {
Self {
state: AssetBodyState::Buffered(bytes),
}
}
}
impl Body for StaticPublicAssetBody {
type Data = Bytes;
type Error = io::Error;
fn poll_frame(
self: Pin<&mut Self>,
ctx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Bytes>, io::Error>>> {
let body = self.get_mut();
match &mut body.state {
AssetBodyState::Buffered(_) => {
let AssetBodyState::Buffered(bytes) =
std::mem::replace(&mut body.state, AssetBodyState::Ended)
else {
unreachable!("the body owns buffered bytes");
};
Poll::Ready(Some(Ok(Frame::data(bytes))))
}
AssetBodyState::File(file_body) => Pin::new(file_body).poll_frame(ctx),
AssetBodyState::LengthOnly(_) | AssetBodyState::Ended => Poll::Ready(None),
}
}
fn size_hint(&self) -> SizeHint {
match &self.state {
AssetBodyState::Buffered(bytes) => SizeHint::with_exact(bytes.len() as u64),
AssetBodyState::File(file_body) => file_body.size_hint(),
AssetBodyState::LengthOnly(length) => SizeHint::with_exact(*length),
AssetBodyState::Ended => SizeHint::with_exact(0),
}
}
}
pub(crate) fn read_content_type(file_name: &str) -> String {
find_media_type_by_extension(Path::new(file_name)).map_or_else(
|| UNKNOWN_CONTENT_TYPE.to_owned(),
|media_type| media_type.as_str().to_owned(),
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct FileRange {
pub(crate) start: u64,
pub(crate) length: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StaticPublicAssetAnswer {
Whole,
Partial(ByteRange),
RangeNotSatisfiable,
}
pub(crate) fn decide_static_public_asset_answer(
headers: &HeaderMap,
length: u64,
) -> StaticPublicAssetAnswer {
let representation = RangeRepresentation { length, etag: None };
let limits = RangeLimits {
max_range_count: NonZeroUsize::MIN,
};
match select_byte_ranges(headers, representation, limits) {
RangeSelection::Whole | RangeSelection::TooManyRanges | RangeSelection::UnsupportedUnit => {
StaticPublicAssetAnswer::Whole
}
RangeSelection::Ranges(ranges) => match ranges.as_slice() {
[range] => StaticPublicAssetAnswer::Partial(*range),
_ => StaticPublicAssetAnswer::Whole,
},
RangeSelection::NotSatisfiable if length == 0 => StaticPublicAssetAnswer::Whole,
RangeSelection::NotSatisfiable | RangeSelection::Invalid => {
StaticPublicAssetAnswer::RangeNotSatisfiable
}
}
}