1use std::fs::{File, Metadata};
2use std::ops::{Deref, DerefMut};
3use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5use std::{error::Error, io, rc::Rc};
6
7#[cfg(unix)]
8use std::os::unix::fs::MetadataExt;
9
10use bitflags::bitflags;
11use futures::stream::TryStreamExt;
12use mime_guess::from_path;
13use ntex::http::body::SizedStream;
14use ntex::http::header::ContentEncoding;
15use ntex::http::{self, StatusCode};
16use ntex::web::{BodyEncoding, ErrorRenderer, HttpRequest, HttpResponse, Responder};
17
18use crate::file_header::{self, Header};
19
20use crate::ChunkedReadFile;
21use crate::range::HttpRange;
22
23bitflags! {
24 #[derive(Clone)]
25 pub(crate) struct Flags: u8 {
26 const ETAG = 0b0000_0001;
27 const LAST_MD = 0b0000_0010;
28 const CONTENT_DISPOSITION = 0b0000_0100;
29 }
30}
31
32impl Default for Flags {
33 fn default() -> Self {
34 Flags::all()
35 }
36}
37
38#[derive(Debug)]
40pub struct NamedFile {
41 path: PathBuf,
42 file: File,
43 modified: Option<SystemTime>,
44 pub(crate) md: Metadata,
45 pub(crate) flags: Flags,
46 pub(crate) status_code: StatusCode,
47 pub(crate) content_type: mime::Mime,
48 pub(crate) content_disposition: file_header::ContentDisposition,
49 pub(crate) encoding: Option<ContentEncoding>,
50}
51
52impl std::fmt::Debug for Flags {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("Flags")
55 .field("etag", &self.contains(Flags::ETAG))
56 .field("last_modified", &self.contains(Flags::LAST_MD))
57 .field("content_disposition", &self.contains(Flags::CONTENT_DISPOSITION))
58 .finish()
59 }
60}
61
62impl NamedFile {
63 pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
85 let path = path.as_ref().to_path_buf();
86
87 let (content_type, content_disposition) = {
90 let filename = match path.file_name() {
91 Some(name) => name.to_string_lossy(),
92 None => {
93 return Err(io::Error::new(
94 io::ErrorKind::InvalidInput,
95 "Provided path has no filename",
96 ));
97 }
98 };
99
100 let ct = from_path(&path).first_or_octet_stream();
101 let disposition = match ct.type_() {
102 mime::IMAGE | mime::TEXT | mime::VIDEO => file_header::DispositionType::Inline,
103 _ => file_header::DispositionType::Attachment,
104 };
105 let parameters = vec![file_header::DispositionParam::Filename(
106 file_header::Charset::Ext(String::from("UTF-8")),
107 None,
108 filename.into_owned().into_bytes(),
109 )];
110 let cd = file_header::ContentDisposition { disposition, parameters };
111 (ct, cd)
112 };
113
114 let md = file.metadata()?;
115 let modified = md.modified().ok();
116 let encoding = None;
117 Ok(NamedFile {
118 path,
119 file,
120 content_type,
121 content_disposition,
122 md,
123 modified,
124 encoding,
125 status_code: StatusCode::OK,
126 flags: Flags::default(),
127 })
128 }
129
130 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
140 Self::from_file(File::open(&path)?, path)
141 }
142
143 #[inline]
145 pub fn file(&self) -> &File {
146 &self.file
147 }
148
149 #[inline]
163 pub fn path(&self) -> &Path {
164 self.path.as_path()
165 }
166
167 pub fn set_status_code(mut self, status: StatusCode) -> Self {
169 self.status_code = status;
170 self
171 }
172
173 #[inline]
176 pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
177 self.content_type = mime_type;
178 self
179 }
180
181 #[inline]
189 pub fn set_content_disposition(mut self, cd: file_header::ContentDisposition) -> Self {
190 self.content_disposition = cd;
191 self.flags.insert(Flags::CONTENT_DISPOSITION);
192 self
193 }
194
195 #[inline]
199 pub fn disable_content_disposition(mut self) -> Self {
200 self.flags.remove(Flags::CONTENT_DISPOSITION);
201 self
202 }
203
204 #[inline]
206 pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
207 self.encoding = Some(enc);
208 self
209 }
210
211 #[inline]
212 pub fn use_etag(mut self, value: bool) -> Self {
216 self.flags.set(Flags::ETAG, value);
217 self
218 }
219
220 #[inline]
221 pub fn use_last_modified(mut self, value: bool) -> Self {
225 self.flags.set(Flags::LAST_MD, value);
226 self
227 }
228
229 pub(crate) fn etag(&self) -> Option<file_header::EntityTag> {
230 self.modified.as_ref().map(|mtime| {
232 let ino = {
233 #[cfg(unix)]
234 {
235 self.md.ino()
236 }
237 #[cfg(not(unix))]
238 {
239 0
240 }
241 };
242
243 let dur = mtime
244 .duration_since(UNIX_EPOCH)
245 .expect("modification time must be after epoch");
246 file_header::EntityTag::strong(format!(
247 "{:x}:{:x}:{:x}:{:x}",
248 ino,
249 self.md.len(),
250 dur.as_secs(),
251 dur.subsec_nanos()
252 ))
253 })
254 }
255
256 pub(crate) fn last_modified(&self) -> Option<file_header::HttpDate> {
257 self.modified.map(|mtime| mtime.into())
258 }
259
260 pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
261 if self.status_code != StatusCode::OK {
262 let mut resp = HttpResponse::build(self.status_code);
263 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
264 resp.header(
265 http::header::CONTENT_DISPOSITION,
266 self.content_disposition.to_string(),
267 );
268 }
269 if let Some(current_encoding) = self.encoding {
270 resp.encoding(current_encoding);
271 }
272 let reader = ChunkedReadFile {
273 size: self.md.len(),
274 offset: 0,
275 file: Some(self.file),
276 fut: None,
277 counter: 0,
278 };
279 resp.header(http::header::CONTENT_TYPE, self.content_type.to_string());
280 return resp.streaming(reader);
281 }
282
283 let etag = if self.flags.contains(Flags::ETAG) { self.etag() } else { None };
284 let last_modified =
285 if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { None };
286
287 let precondition_failed = if !any_match(etag.as_ref(), req) {
289 true
290 } else if let (Some(ref m), Some(file_header::IfUnmodifiedSince(ref since))) = {
291 let mut header = None;
292 for hdr in req.headers().get_all(http::header::IF_UNMODIFIED_SINCE) {
293 if let Ok(v) = file_header::IfUnmodifiedSince::parse_header(
294 &file_header::Raw::from(hdr.as_bytes()),
295 ) {
296 header = Some(v);
297 break;
298 }
299 }
300
301 (last_modified, header)
302 } {
303 let t1: SystemTime = (*m).into();
304 let t2: SystemTime = (*since).into();
305 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
306 (Ok(t1), Ok(t2)) => t1 > t2,
307 _ => false,
308 }
309 } else {
310 false
311 };
312
313 let not_modified = if !none_match(etag.as_ref(), req) {
315 true
316 } else if req.headers().contains_key(&http::header::IF_NONE_MATCH) {
317 false
318 } else if let (Some(ref m), Some(file_header::IfModifiedSince(ref since))) = {
319 let mut header = None;
320 for hdr in req.headers().get_all(http::header::IF_MODIFIED_SINCE) {
321 if let Ok(v) = file_header::IfModifiedSince::parse_header(
322 &file_header::Raw::from(hdr.as_bytes()),
323 ) {
324 header = Some(v);
325 break;
326 }
327 }
328 (last_modified, header)
329 } {
330 let t1: SystemTime = (*m).into();
331 let t2: SystemTime = (*since).into();
332 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
333 (Ok(t1), Ok(t2)) => t1 <= t2,
334 _ => false,
335 }
336 } else {
337 false
338 };
339
340 let mut resp = HttpResponse::build(self.status_code);
341 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
342 resp.header(
343 http::header::CONTENT_DISPOSITION,
344 self.content_disposition.to_string(),
345 );
346 }
347 if let Some(current_encoding) = self.encoding {
349 resp.encoding(current_encoding);
350 }
351 if let Some(lm) = last_modified {
352 resp.header(http::header::LAST_MODIFIED, file_header::LastModified(lm).to_string());
353 }
354 if let Some(etag) = etag {
355 resp.header(http::header::ETAG, file_header::ETag(etag).to_string());
356 }
357
358 resp.header(http::header::CONTENT_TYPE, self.content_type.to_string());
359 resp.header(http::header::ACCEPT_RANGES, "bytes");
360
361 let mut length = self.md.len();
362 let mut offset = 0;
363
364 if let Some(ranges) = req.headers().get(&http::header::RANGE) {
366 if let Ok(rangesheader) = ranges.to_str() {
367 if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
368 length = rangesvec[0].length;
369 offset = rangesvec[0].start;
370 resp.encoding(ContentEncoding::Identity);
371 resp.header(
372 http::header::CONTENT_RANGE,
373 format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
374 );
375 } else {
376 resp.header(http::header::CONTENT_RANGE, format!("bytes */{}", length));
377 return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
378 };
379 } else {
380 return resp.status(StatusCode::BAD_REQUEST).finish();
381 };
382 };
383
384 if precondition_failed {
385 return resp.status(StatusCode::PRECONDITION_FAILED).finish();
386 } else if not_modified {
387 return resp.status(StatusCode::NOT_MODIFIED).finish();
388 }
389
390 let reader = ChunkedReadFile {
391 offset,
392 size: length,
393 file: Some(self.file),
394 fut: None,
395 counter: 0,
396 };
397 if offset != 0 || length != self.md.len() {
398 resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)
399 } else {
400 resp.body(SizedStream::new(
401 length,
402 reader.map_err(|e| {
403 let e: Rc<dyn Error> = Rc::new(e);
404 e
405 }),
406 ))
407 }
408 }
409}
410
411impl Deref for NamedFile {
412 type Target = File;
413
414 fn deref(&self) -> &File {
415 &self.file
416 }
417}
418
419impl DerefMut for NamedFile {
420 fn deref_mut(&mut self) -> &mut File {
421 &mut self.file
422 }
423}
424
425fn any_match(etag: Option<&file_header::EntityTag>, req: &HttpRequest) -> bool {
427 if let Some(val) = req.headers().get(http::header::IF_MATCH) {
428 let hdr = ::http::HeaderValue::from(val);
429 if let Ok(val) = file_header::IfMatch::parse_header(&&hdr) {
430 match val {
431 file_header::IfMatch::Any => return true,
432 file_header::IfMatch::Items(ref items) => {
433 if let Some(some_etag) = etag {
434 for item in items {
435 if item.strong_eq(some_etag) {
436 return true;
437 }
438 }
439 }
440 }
441 };
442 return false;
443 }
444 }
445 true
446}
447
448fn none_match(etag: Option<&file_header::EntityTag>, req: &HttpRequest) -> bool {
450 if let Some(val) = req.headers().get(http::header::IF_NONE_MATCH) {
451 let hdr = ::http::HeaderValue::from(val);
452 if let Ok(val) = file_header::IfNoneMatch::parse_header(&&hdr) {
453 return match val {
454 file_header::IfNoneMatch::Any => false,
455 file_header::IfNoneMatch::Items(ref items) => {
456 if let Some(some_etag) = etag {
457 for item in items {
458 if item.weak_eq(some_etag) {
459 return false;
460 }
461 }
462 }
463 true
464 }
465 };
466 }
467 }
468 true
469}
470
471impl<Err: ErrorRenderer> Responder<Err> for NamedFile {
472 async fn respond_to(self, req: &HttpRequest) -> HttpResponse {
473 self.into_response(req)
474 }
475}