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::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: 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::AUDIO | mime::VIDEO => {
103 header::DispositionType::Inline
104 }
105 mime::APPLICATION => match ct.subtype() {
106 mime::JAVASCRIPT | mime::JSON => header::DispositionType::Inline,
107 name if name == "wasm" || name == "xhtml" => {
108 header::DispositionType::Inline
109 }
110 _ => header::DispositionType::Attachment,
111 },
112 _ => header::DispositionType::Attachment,
113 };
114
115 let filename_s = filename
117 .replace('\n', "%0A") .replace('\x0B', "%0B") .replace('\x0C', "%0C") .replace('\r', "%0D"); let mut parameters = vec![header::DispositionParam::Filename(filename_s)];
122
123 if !filename.is_ascii() {
124 parameters.push(header::DispositionParam::FilenameExt(
125 header::parsing::ExtendedValue {
126 charset: header::Charset::Ext(String::from("UTF-8")),
127 language_tag: None,
128 value: filename.into_owned().into_bytes(),
129 },
130 ))
131 }
132
133 let cd = header::ContentDisposition { disposition, parameters };
134 (ct, cd)
135 };
136
137 let md = file.metadata()?;
138 let modified = md.modified().ok();
139 let encoding = None;
140 Ok(NamedFile {
141 path,
142 file,
143 content_type,
144 content_disposition,
145 md,
146 modified,
147 encoding,
148 status_code: StatusCode::OK,
149 flags: Flags::default(),
150 })
151 }
152
153 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
163 Self::from_file(File::open(&path)?, path)
164 }
165
166 #[inline]
168 pub fn file(&self) -> &File {
169 &self.file
170 }
171
172 #[inline]
186 pub fn path(&self) -> &Path {
187 self.path.as_path()
188 }
189
190 pub fn set_status_code(mut self, status: StatusCode) -> Self {
192 self.status_code = status;
193 self
194 }
195
196 #[inline]
199 pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
200 self.content_type = mime_type;
201 self
202 }
203
204 #[inline]
212 pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
213 self.content_disposition = cd;
214 self.flags.insert(Flags::CONTENT_DISPOSITION);
215 self
216 }
217
218 #[inline]
222 pub fn disable_content_disposition(mut self) -> Self {
223 self.flags.remove(Flags::CONTENT_DISPOSITION);
224 self
225 }
226
227 #[inline]
229 pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
230 self.encoding = Some(enc);
231 self
232 }
233
234 #[inline]
235 pub fn use_etag(mut self, value: bool) -> Self {
239 self.flags.set(Flags::ETAG, value);
240 self
241 }
242
243 #[inline]
244 pub fn use_last_modified(mut self, value: bool) -> Self {
248 self.flags.set(Flags::LAST_MD, value);
249 self
250 }
251
252 pub(crate) fn etag(&self) -> Option<header::EntityTag> {
253 self.modified.as_ref().map(|mtime| {
255 let ino = {
256 #[cfg(unix)]
257 {
258 self.md.ino()
259 }
260 #[cfg(not(unix))]
261 {
262 0
263 }
264 };
265
266 let dur = mtime
267 .duration_since(UNIX_EPOCH)
268 .expect("modification time must be after epoch");
269 header::EntityTag::strong(format!(
270 "{:x}:{:x}:{:x}:{:x}",
271 ino,
272 self.md.len(),
273 dur.as_secs(),
274 dur.subsec_nanos()
275 ))
276 })
277 }
278
279 pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
280 self.modified.map(|mtime| mtime.into())
281 }
282
283 pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
284 if self.status_code != StatusCode::OK {
285 let mut resp = HttpResponse::build(self.status_code);
286 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
287 resp.header(
288 http::header::CONTENT_DISPOSITION,
289 self.content_disposition.to_string(),
290 );
291 }
292 if let Some(current_encoding) = self.encoding {
293 resp.encoding(current_encoding);
294 }
295 let reader = ChunkedReadFile {
296 size: self.md.len(),
297 offset: 0,
298 file: Some(self.file),
299 fut: None,
300 counter: 0,
301 };
302 resp.header(http::header::CONTENT_TYPE, self.content_type.to_string());
303 return resp.streaming(reader);
304 }
305
306 let etag = if self.flags.contains(Flags::ETAG) { self.etag() } else { None };
307 let last_modified =
308 if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { None };
309
310 let precondition_failed = if !any_match(etag.as_ref(), req) {
312 true
313 } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = {
314 let mut header = None;
315 for hdr in req.headers().get_all(http::header::IF_UNMODIFIED_SINCE) {
316 if let Ok(v) =
317 header::IfUnmodifiedSince::parse_header(&header::Raw::from(hdr.as_bytes()))
318 {
319 header = Some(v);
320 break;
321 }
322 }
323
324 (last_modified, header)
325 } {
326 let t1: SystemTime = (*m).into();
327 let t2: SystemTime = (*since).into();
328 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
329 (Ok(t1), Ok(t2)) => t1 > t2,
330 _ => false,
331 }
332 } else {
333 false
334 };
335
336 let not_modified = if !none_match(etag.as_ref(), req) {
338 true
339 } else if req.headers().contains_key(&http::header::IF_NONE_MATCH) {
340 false
341 } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = {
342 let mut header = None;
343 for hdr in req.headers().get_all(http::header::IF_MODIFIED_SINCE) {
344 if let Ok(v) =
345 header::IfModifiedSince::parse_header(&header::Raw::from(hdr.as_bytes()))
346 {
347 header = Some(v);
348 break;
349 }
350 }
351 (last_modified, header)
352 } {
353 let t1: SystemTime = (*m).into();
354 let t2: SystemTime = (*since).into();
355 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
356 (Ok(t1), Ok(t2)) => t1 <= t2,
357 _ => false,
358 }
359 } else {
360 false
361 };
362
363 let mut resp = HttpResponse::build(self.status_code);
364 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
365 resp.header(
366 http::header::CONTENT_DISPOSITION,
367 self.content_disposition.to_string(),
368 );
369 }
370 if let Some(current_encoding) = self.encoding {
372 resp.encoding(current_encoding);
373 }
374 if let Some(lm) = last_modified {
375 resp.header(http::header::LAST_MODIFIED, header::LastModified(lm).to_string());
376 }
377 if let Some(etag) = etag {
378 resp.header(http::header::ETAG, header::ETag(etag).to_string());
379 }
380
381 resp.header(http::header::CONTENT_TYPE, self.content_type.to_string());
382 resp.header(http::header::ACCEPT_RANGES, "bytes");
383
384 let mut length = self.md.len();
385 let mut offset = 0;
386
387 if let Some(ranges) = req.headers().get(&http::header::RANGE) {
389 if let Ok(rangesheader) = ranges.to_str() {
390 if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
391 length = rangesvec[0].length;
392 offset = rangesvec[0].start;
393 resp.encoding(ContentEncoding::Identity);
394 resp.header(
395 http::header::CONTENT_RANGE,
396 format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
397 );
398 } else {
399 resp.header(http::header::CONTENT_RANGE, format!("bytes */{}", length));
400 return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
401 };
402 } else {
403 return resp.status(StatusCode::BAD_REQUEST).finish();
404 };
405 };
406
407 if precondition_failed {
408 return resp.status(StatusCode::PRECONDITION_FAILED).finish();
409 } else if not_modified {
410 return resp.status(StatusCode::NOT_MODIFIED).finish();
411 }
412
413 let reader = ChunkedReadFile {
414 offset,
415 size: length,
416 file: Some(self.file),
417 fut: None,
418 counter: 0,
419 };
420 if offset != 0 || length != self.md.len() {
421 resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)
422 } else {
423 resp.body(SizedStream::new(
424 length,
425 reader.map_err(|e| {
426 let e: Rc<dyn Error> = Rc::new(e);
427 e
428 }),
429 ))
430 }
431 }
432}
433
434impl Deref for NamedFile {
435 type Target = File;
436
437 fn deref(&self) -> &File {
438 &self.file
439 }
440}
441
442impl DerefMut for NamedFile {
443 fn deref_mut(&mut self) -> &mut File {
444 &mut self.file
445 }
446}
447
448fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
450 if let Some(val) = req.headers().get(http::header::IF_MATCH) {
451 let hdr = ::http::HeaderValue::from(val);
452 if let Ok(val) = header::IfMatch::parse_header(&&hdr) {
453 match val {
454 header::IfMatch::Any => return true,
455 header::IfMatch::Items(ref items) => {
456 if let Some(some_etag) = etag {
457 for item in items {
458 if item.strong_eq(some_etag) {
459 return true;
460 }
461 }
462 }
463 }
464 };
465 return false;
466 }
467 }
468 true
469}
470
471fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
473 if let Some(val) = req.headers().get(http::header::IF_NONE_MATCH) {
474 let hdr = ::http::HeaderValue::from(val);
475 if let Ok(val) = header::IfNoneMatch::parse_header(&&hdr) {
476 return match val {
477 header::IfNoneMatch::Any => false,
478 header::IfNoneMatch::Items(ref items) => {
479 if let Some(some_etag) = etag {
480 for item in items {
481 if item.weak_eq(some_etag) {
482 return false;
483 }
484 }
485 }
486 true
487 }
488 };
489 }
490 }
491 true
492}
493
494impl<Err: ErrorRenderer> Responder<Err> for NamedFile {
495 async fn respond_to(self, req: &HttpRequest) -> HttpResponse {
496 self.into_response(req)
497 }
498}