use http::header;
use httpdate::fmt_http_date;
use mime_guess::MimeGuess;
use pingora_http::{ResponseHeader, StatusCode};
use pingora_proxy::Session;
use std::io::{Error, ErrorKind};
use std::path::Path;
use std::time::SystemTime;
#[derive(Debug)]
pub struct Metadata {
pub mime: MimeGuess,
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));
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_common_headers(
&self,
header: &mut ResponseHeader,
) -> Result<(), Box<pingora_core::Error>> {
header.append_header(
header::CONTENT_TYPE,
self.mime.first_or_octet_stream().as_ref(),
)?;
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,
) -> Result<Box<ResponseHeader>, Box<pingora_core::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_common_headers(&mut header)?;
Ok(Box::new(header))
}
pub(crate) fn to_partial_content_header(
&self,
start: u64,
end: u64,
) -> Result<Box<ResponseHeader>, Box<pingora_core::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_common_headers(&mut header)?;
Ok(Box::new(header))
}
pub(crate) fn to_custom_header(
&self,
status: StatusCode,
) -> Result<Box<ResponseHeader>, Box<pingora_core::Error>> {
let mut header = ResponseHeader::build(status, Some(4))?;
self.add_common_headers(&mut header)?;
Ok(Box::new(header))
}
}