use http::{header, status::StatusCode};
use httpdate::fmt_http_date;
use mime_guess::Mime;
use pingora::http::ResponseHeader;
use std::io::{Error, ErrorKind};
use std::path::Path;
use std::time::SystemTime;
use pingora::proxy::Session;
#[derive(Debug)]
pub struct Metadata {
pub mime: Mime,
pub size: u64,
pub modified: Option<String>,
pub etag: String,
}
impl Metadata {
pub fn from_path<P: AsRef<Path> + ?Sized>(
path: &P,
orig_path: Option<&P>,
) -> Result<Self, Error> {
let meta = path.as_ref().metadata()?;
if !meta.is_file() {
return Err(ErrorKind::InvalidInput.into());
}
let mime = mime_guess::from_path(orig_path.unwrap_or(path)).first_or_octet_stream();
let size = meta.len();
let modified = meta.modified().ok().map(fmt_http_date);
let etag = format!(
"\"{:x}-{:x}\"",
meta.modified()
.ok()
.and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
.map_or(0, |duration| duration.as_secs()),
meta.len()
);
Ok(Self {
mime,
size,
modified,
etag,
})
}
pub fn has_failed_precondition(&self, session: &Session) -> bool {
let headers = &session.req_header().headers;
if let Some(value) = headers
.get(header::IF_MATCH)
.and_then(|value| value.to_str().ok())
{
value != "*"
&& value
.split(',')
.map(str::trim)
.all(|value| value != self.etag)
} else if let Some(value) = headers
.get(header::IF_UNMODIFIED_SINCE)
.and_then(|value| value.to_str().ok())
{
self.modified
.as_ref()
.is_some_and(|modified| modified != value)
} else {
false
}
}
pub fn is_not_modified(&self, session: &Session) -> bool {
let headers = &session.req_header().headers;
if let Some(value) = headers
.get(header::IF_NONE_MATCH)
.and_then(|value| value.to_str().ok())
{
value == "*"
|| value
.split(',')
.map(str::trim)
.any(|value| value == self.etag)
} else if let Some(value) = headers
.get(header::IF_MODIFIED_SINCE)
.and_then(|value| value.to_str().ok())
{
self.modified
.as_ref()
.is_some_and(|modified| modified == value)
} else {
false
}
}
#[inline(always)]
fn add_content_type(
&self,
header: &mut ResponseHeader,
charset: Option<&str>,
) -> Result<(), Box<pingora::Error>> {
if let Some(charset) = charset {
header.append_header(
header::CONTENT_TYPE,
format!("{};charset={charset}", self.mime.as_ref()),
)?;
} else {
header.append_header(header::CONTENT_TYPE, self.mime.as_ref())?;
}
Ok(())
}
#[inline(always)]
fn add_etag(
&self,
header: &mut ResponseHeader,
) -> Result<(), Box<pingora::Error>> {
if let Some(modified) = &self.modified {
header.append_header(header::LAST_MODIFIED, modified)?;
}
header.append_header(header::ETAG, &self.etag)?;
Ok(())
}
pub(crate) fn to_response_header(
&self,
charset: Option<&str>,
) -> Result<Box<ResponseHeader>, Box<pingora::Error>> {
let mut header = ResponseHeader::build(StatusCode::OK, Some(8))?;
header.append_header(header::CONTENT_LENGTH, self.size.to_string())?;
header.append_header(header::ACCEPT_RANGES, "bytes")?;
self.add_content_type(&mut header, charset)?;
self.add_etag(&mut header)?;
Ok(Box::new(header))
}
pub(crate) fn to_partial_content_header(
&self,
charset: Option<&str>,
start: u64,
end: u64,
) -> Result<Box<ResponseHeader>, Box<pingora::Error>> {
let mut header = ResponseHeader::build(StatusCode::PARTIAL_CONTENT, Some(8))?;
header.append_header(header::CONTENT_LENGTH, (end - start + 1).to_string())?;
header.append_header(
header::CONTENT_RANGE,
format!("bytes {start}-{end}/{}", self.size),
)?;
self.add_content_type(&mut header, charset)?;
self.add_etag(&mut header)?;
Ok(Box::new(header))
}
pub(crate) fn to_not_satisfiable_header(
&self,
charset: Option<&str>,
) -> Result<Box<ResponseHeader>, Box<pingora::Error>> {
let mut header = ResponseHeader::build(StatusCode::RANGE_NOT_SATISFIABLE, Some(4))?;
header.append_header(header::CONTENT_RANGE, format!("bytes */{}", self.size))?;
self.add_content_type(&mut header, charset)?;
self.add_etag(&mut header)?;
Ok(Box::new(header))
}
pub(crate) fn to_custom_header(
&self,
status: StatusCode,
) -> Result<Box<ResponseHeader>, Box<pingora::Error>> {
let mut header = ResponseHeader::build(status, Some(4))?;
self.add_etag(&mut header)?;
Ok(Box::new(header))
}
}