use anyhow::{Context, Result};
use chrono::{DateTime, Local, Utc};
use futures::Stream;
use http::response::Builder as HttpResponseBuilder;
use hyper::body::Body;
use hyper::body::Bytes;
use std::mem::MaybeUninit;
use std::pin::Pin;
use std::task::{self, Poll};
use tokio::io::{AsyncRead, ReadBuf};
use super::file::File;
const FILE_BUFFER_SIZE: usize = 8 * 1024;
pub type FileBuffer = Box<[MaybeUninit<u8>; FILE_BUFFER_SIZE]>;
#[allow(dead_code)]
pub enum CacheControlDirective {
MustRevalidate,
NoCache,
NoStore,
NoTransform,
Public,
Private,
ProxyRavalidate,
MaxAge(u64),
SMaxAge(u64),
}
impl ToString for CacheControlDirective {
fn to_string(&self) -> String {
match &self {
Self::MustRevalidate => String::from("must-revalidate"),
Self::NoCache => String::from("no-cache"),
Self::NoStore => String::from("no-store"),
Self::NoTransform => String::from("no-transform"),
Self::Public => String::from("public"),
Self::Private => String::from("private"),
Self::ProxyRavalidate => String::from("proxy-revalidate"),
Self::MaxAge(age) => format!("max-age={}", age),
Self::SMaxAge(age) => format!("s-maxage={}", age),
}
}
}
pub struct ResponseHeaders {
cache_control: String,
content_length: u64,
content_type: String,
etag: String,
last_modified: String,
}
impl ResponseHeaders {
pub fn new(
file: &File,
cache_control_directive: CacheControlDirective,
) -> Result<ResponseHeaders> {
let last_modified = file.last_modified()?;
Ok(ResponseHeaders {
cache_control: cache_control_directive.to_string(),
content_length: ResponseHeaders::content_length(file),
content_type: ResponseHeaders::content_type(file),
etag: ResponseHeaders::etag(file, &last_modified),
last_modified: ResponseHeaders::last_modified(&last_modified),
})
}
fn content_length(file: &File) -> u64 {
file.size()
}
fn content_type(file: &File) -> String {
file.mime().to_string()
}
fn etag(file: &File, last_modified: &DateTime<Local>) -> String {
format!(
"W/\"{0:x}-{1:x}.{2:x}\"",
file.size(),
last_modified.timestamp(),
last_modified.timestamp_subsec_nanos(),
)
}
fn last_modified(last_modified: &DateTime<Local>) -> String {
format!(
"{} GMT",
last_modified
.with_timezone(&Utc)
.format("%a, %e %b %Y %H:%M:%S")
)
}
}
pub async fn make_http_file_response(
file: File,
cache_control_directive: CacheControlDirective,
) -> Result<hyper::http::Response<Body>> {
let headers = ResponseHeaders::new(&file, cache_control_directive)?;
let builder = HttpResponseBuilder::new()
.header(http::header::CONTENT_LENGTH, headers.content_length)
.header(http::header::CACHE_CONTROL, headers.cache_control)
.header(http::header::CONTENT_TYPE, headers.content_type)
.header(http::header::ETAG, headers.etag)
.header(http::header::LAST_MODIFIED, headers.last_modified);
let body = file_bytes_into_http_body(file).await;
let response = builder
.body(body)
.context("Failed to build HTTP File Response")?;
Ok(response)
}
pub async fn file_bytes_into_http_body(file: File) -> Body {
let byte_stream = ByteStream {
file: file.file,
buffer: Box::new([MaybeUninit::uninit(); FILE_BUFFER_SIZE]),
};
Body::wrap_stream(byte_stream)
}
pub struct ByteStream {
file: tokio::fs::File,
buffer: FileBuffer,
}
impl Stream for ByteStream {
type Item = Result<Bytes>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
let ByteStream {
ref mut file,
ref mut buffer,
} = *self;
let mut read_buffer = ReadBuf::uninit(&mut buffer[..]);
match Pin::new(file).poll_read(cx, &mut read_buffer) {
Poll::Ready(Ok(())) => {
let filled = read_buffer.filled();
if filled.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::copy_from_slice(filled))))
}
}
Poll::Ready(Err(error)) => Poll::Ready(Some(Err(error.into()))),
Poll::Pending => Poll::Pending,
}
}
}