static_files_module/
metadata.rs1use http::header;
18use httpdate::fmt_http_date;
19use mime_guess::MimeGuess;
20use pingora_http::{ResponseHeader, StatusCode};
21use pingora_proxy::Session;
22use std::io::{Error, ErrorKind};
23use std::path::Path;
24use std::time::SystemTime;
25
26#[derive(Debug)]
28pub struct Metadata {
29 pub mime: MimeGuess,
31 pub size: u64,
33 pub modified: Option<String>,
36 pub etag: String,
38}
39
40impl Metadata {
41 pub fn from_path<P: AsRef<Path> + ?Sized>(
47 path: &P,
48 orig_path: Option<&P>,
49 ) -> Result<Self, Error> {
50 let meta = path.as_ref().metadata()?;
51
52 if !meta.is_file() {
53 return Err(ErrorKind::InvalidInput.into());
54 }
55
56 let mime = mime_guess::from_path(orig_path.unwrap_or(path));
57 let size = meta.len();
58 let modified = meta.modified().ok().map(fmt_http_date);
59 let etag = format!(
60 "\"{:x}-{:x}\"",
61 meta.modified()
62 .ok()
63 .and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
64 .map_or(0, |duration| duration.as_secs()),
65 meta.len()
66 );
67
68 Ok(Self {
69 mime,
70 size,
71 modified,
72 etag,
73 })
74 }
75
76 pub fn has_failed_precondition(&self, session: &Session) -> bool {
79 let headers = &session.req_header().headers;
80 if let Some(value) = headers
81 .get(header::IF_MATCH)
82 .and_then(|value| value.to_str().ok())
83 {
84 value != "*"
85 && value
86 .split(',')
87 .map(str::trim)
88 .all(|value| value != self.etag)
89 } else if let Some(value) = headers
90 .get(header::IF_UNMODIFIED_SINCE)
91 .and_then(|value| value.to_str().ok())
92 {
93 self.modified
94 .as_ref()
95 .is_some_and(|modified| modified != value)
96 } else {
97 false
98 }
99 }
100
101 pub fn is_not_modified(&self, session: &Session) -> bool {
104 let headers = &session.req_header().headers;
105 if let Some(value) = headers
106 .get(header::IF_NONE_MATCH)
107 .and_then(|value| value.to_str().ok())
108 {
109 value == "*"
110 || value
111 .split(',')
112 .map(str::trim)
113 .any(|value| value == self.etag)
114 } else if let Some(value) = headers
115 .get(header::IF_MODIFIED_SINCE)
116 .and_then(|value| value.to_str().ok())
117 {
118 self.modified
119 .as_ref()
120 .is_some_and(|modified| modified == value)
121 } else {
122 false
123 }
124 }
125
126 #[inline(always)]
127 fn add_common_headers(
128 &self,
129 header: &mut ResponseHeader,
130 ) -> Result<(), Box<pingora_core::Error>> {
131 header.append_header(
132 header::CONTENT_TYPE,
133 self.mime.first_or_octet_stream().as_ref(),
134 )?;
135 if let Some(modified) = &self.modified {
136 header.append_header(header::LAST_MODIFIED, modified)?;
137 }
138 header.append_header(header::ETAG, &self.etag)?;
139 Ok(())
140 }
141
142 pub(crate) fn to_response_header(
144 &self,
145 ) -> Result<Box<ResponseHeader>, Box<pingora_core::Error>> {
146 let mut header = ResponseHeader::build(StatusCode::OK, Some(8))?;
147 header.append_header(header::CONTENT_LENGTH, self.size.to_string())?;
148 header.append_header(header::ACCEPT_RANGES, "bytes")?;
149 self.add_common_headers(&mut header)?;
150 Ok(Box::new(header))
151 }
152
153 pub(crate) fn to_partial_content_header(
155 &self,
156 start: u64,
157 end: u64,
158 ) -> Result<Box<ResponseHeader>, Box<pingora_core::Error>> {
159 let mut header = ResponseHeader::build(StatusCode::PARTIAL_CONTENT, Some(8))?;
160 header.append_header(header::CONTENT_LENGTH, (end - start + 1).to_string())?;
161 header.append_header(
162 header::CONTENT_RANGE,
163 format!("bytes {start}-{end}/{}", self.size),
164 )?;
165 self.add_common_headers(&mut header)?;
166 Ok(Box::new(header))
167 }
168
169 pub(crate) fn to_custom_header(
172 &self,
173 status: StatusCode,
174 ) -> Result<Box<ResponseHeader>, Box<pingora_core::Error>> {
175 let mut header = ResponseHeader::build(status, Some(4))?;
176 self.add_common_headers(&mut header)?;
177 Ok(Box::new(header))
178 }
179}