use super::FileBytesStream;
use crate::util::DateTimeHttp;
use chrono::{offset::Local as LocalTz, DateTime, SubsecRound};
use http::response::Builder as ResponseBuilder;
use http::{header, HeaderMap, Method, Request, Response, Result, StatusCode};
use hyper::Body;
use std::fs::Metadata;
use std::time::{Duration, UNIX_EPOCH};
use tokio::fs::File;
const MIN_VALID_MTIME: Duration = Duration::from_secs(2);
#[derive(Clone, Debug, Default)]
pub struct FileResponseBuilder {
pub cache_headers: Option<u32>,
pub is_head: bool,
pub if_modified_since: Option<DateTime<LocalTz>>,
}
impl FileResponseBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn request<B>(&mut self, req: &Request<B>) -> &mut Self {
self.request_parts(req.method(), req.headers())
}
pub fn request_parts(&mut self, method: &Method, headers: &HeaderMap) -> &mut Self {
self.request_method(method);
self.request_headers(headers);
self
}
pub fn request_method(&mut self, method: &Method) -> &mut Self {
self.is_head = *method == Method::HEAD;
self
}
pub fn request_headers(&mut self, headers: &HeaderMap) -> &mut Self {
self.if_modified_since_header(headers.get(header::IF_MODIFIED_SINCE));
self
}
pub fn cache_headers(&mut self, value: Option<u32>) -> &mut Self {
self.cache_headers = value;
self
}
pub fn is_head(&mut self, value: bool) -> &mut Self {
self.is_head = value;
self
}
pub fn if_modified_since(&mut self, value: Option<DateTime<LocalTz>>) -> &mut Self {
self.if_modified_since = value;
self
}
pub fn if_modified_since_header(&mut self, value: Option<&header::HeaderValue>) -> &mut Self {
self.if_modified_since = value
.and_then(|v| v.to_str().ok())
.and_then(|v| DateTime::parse_from_rfc2822(v).ok())
.map(|v| v.with_timezone(&LocalTz));
self
}
pub fn build(&self, file: File, metadata: Metadata) -> Result<Response<Body>> {
let mut res = ResponseBuilder::new();
let modified = metadata.modified().ok().filter(|v| {
v.duration_since(UNIX_EPOCH)
.ok()
.filter(|v| v >= &MIN_VALID_MTIME)
.is_some()
});
if let Some(modified) = modified {
let modified: DateTime<LocalTz> = modified.into();
match self.if_modified_since {
Some(v) if modified.trunc_subsecs(0) <= v.trunc_subsecs(0) => {
return ResponseBuilder::new()
.status(StatusCode::NOT_MODIFIED)
.body(Body::empty())
}
_ => {}
}
res = res
.header(header::LAST_MODIFIED, modified.to_http_date())
.header(
header::ETAG,
format!(
"W/\"{0:x}-{1:x}.{2:x}\"",
metadata.len(),
modified.timestamp(),
modified.timestamp_subsec_nanos()
),
);
}
res = res.header(header::CONTENT_LENGTH, format!("{}", metadata.len()));
if let Some(seconds) = self.cache_headers {
res = res.header(
header::CACHE_CONTROL,
format!("public, max-age={}", seconds),
);
}
res.body(if self.is_head {
Body::empty()
} else {
FileBytesStream::new(file).into_body()
})
}
}