1use std::{
2 fs::Metadata,
3 io,
4 path::{Path, PathBuf},
5 time::{SystemTime, UNIX_EPOCH},
6};
7
8use actix_web::{
9 body::{self, BoxBody, SizedStream},
10 dev::{
11 self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
12 ServiceResponse,
13 },
14 http::{
15 header::{
16 self, Charset, ContentDisposition, ContentEncoding, DispositionParam, DispositionType,
17 ExtendedValue, HeaderValue,
18 },
19 StatusCode,
20 },
21 Error, HttpMessage, HttpRequest, HttpResponse, Responder,
22};
23use bitflags::bitflags;
24use derive_more::{Deref, DerefMut};
25use futures_core::future::LocalBoxFuture;
26use mime::Mime;
27
28use crate::{encoding::equiv_utf8_text, range::HttpRange};
29
30bitflags! {
31 #[derive(Debug, Clone, Copy)]
32 pub(crate) struct Flags: u8 {
33 const ETAG = 0b0000_0001;
34 const LAST_MD = 0b0000_0010;
35 const CONTENT_DISPOSITION = 0b0000_0100;
36 const PREFER_UTF8 = 0b0000_1000;
37 }
38}
39
40impl Default for Flags {
41 fn default() -> Self {
42 Flags::from_bits_truncate(0b0000_1111)
43 }
44}
45
46#[derive(Debug, Deref, DerefMut)]
71pub struct NamedFile {
72 #[deref]
73 #[deref_mut]
74 file: File,
75 path: PathBuf,
76 modified: Option<SystemTime>,
77 pub(crate) md: Metadata,
78 pub(crate) flags: Flags,
79 pub(crate) status_code: StatusCode,
80 pub(crate) content_type: Mime,
81 pub(crate) content_disposition: ContentDisposition,
82 pub(crate) encoding: Option<ContentEncoding>,
83}
84
85#[cfg(not(feature = "experimental-io-uring"))]
86pub(crate) use std::fs::File;
87
88#[cfg(feature = "experimental-io-uring")]
89pub(crate) use tokio_uring::fs::File;
90
91use super::chunked;
92
93impl NamedFile {
94 pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
115 let path = path.as_ref().to_path_buf();
116
117 let (content_type, content_disposition) = {
120 let filename = match path.file_name() {
121 Some(name) => name.to_string_lossy(),
122 None => {
123 return Err(io::Error::new(
124 io::ErrorKind::InvalidInput,
125 "Provided path has no filename",
126 ));
127 }
128 };
129
130 let ct = mime_guess::from_path(&path).first_or_octet_stream();
131
132 let disposition = match ct.type_() {
133 mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
134 mime::APPLICATION => match ct.subtype() {
135 mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
136 name if name == "wasm" || name == "xhtml" => DispositionType::Inline,
137 _ => DispositionType::Attachment,
138 },
139 _ => DispositionType::Attachment,
140 };
141
142 let filename_s = filename
144 .replace('\n', "%0A") .replace('\x0B', "%0B") .replace('\x0C', "%0C") .replace('\r', "%0D"); let mut parameters = vec![DispositionParam::Filename(filename_s)];
149
150 if !filename.is_ascii() {
151 parameters.push(DispositionParam::FilenameExt(ExtendedValue {
152 charset: Charset::Ext(String::from("UTF-8")),
153 language_tag: None,
154 value: filename.into_owned().into_bytes(),
155 }))
156 }
157
158 let cd = ContentDisposition {
159 disposition,
160 parameters,
161 };
162
163 (ct, cd)
164 };
165
166 let md = {
167 #[cfg(not(feature = "experimental-io-uring"))]
168 {
169 file.metadata()?
170 }
171
172 #[cfg(feature = "experimental-io-uring")]
173 {
174 use std::os::unix::prelude::{AsRawFd, FromRawFd};
175
176 let fd = file.as_raw_fd();
177
178 unsafe {
180 let file = std::fs::File::from_raw_fd(fd);
181 let md = file.metadata();
182 std::mem::forget(file);
185 md?
186 }
187 }
188 };
189
190 let modified = md.modified().ok();
191 let encoding = None;
192
193 Ok(NamedFile {
194 path,
195 file,
196 content_type,
197 content_disposition,
198 md,
199 modified,
200 encoding,
201 status_code: StatusCode::OK,
202 flags: Flags::default(),
203 })
204 }
205
206 #[cfg(not(feature = "experimental-io-uring"))]
214 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
215 let file = File::open(&path)?;
216 Self::from_file(file, path)
217 }
218
219 pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
232 let file = {
233 #[cfg(not(feature = "experimental-io-uring"))]
234 {
235 File::open(&path)?
236 }
237
238 #[cfg(feature = "experimental-io-uring")]
239 {
240 File::open(&path).await?
241 }
242 };
243
244 Self::from_file(file, path)
245 }
246
247 #[inline]
249 pub fn file(&self) -> &File {
250 &self.file
251 }
252
253 #[inline]
267 pub fn path(&self) -> &Path {
268 self.path.as_path()
269 }
270
271 #[inline]
276 pub fn modified(&self) -> Option<SystemTime> {
277 self.modified
278 }
279
280 #[inline]
282 pub fn metadata(&self) -> &Metadata {
283 &self.md
284 }
285
286 #[inline]
288 pub fn content_type(&self) -> &Mime {
289 &self.content_type
290 }
291
292 #[inline]
294 pub fn content_disposition(&self) -> &ContentDisposition {
295 &self.content_disposition
296 }
297
298 #[inline]
303 pub fn content_encoding(&self) -> Option<ContentEncoding> {
304 self.encoding
305 }
306
307 #[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")]
309 pub fn set_status_code(mut self, status: StatusCode) -> Self {
310 self.status_code = status;
311 self
312 }
313
314 #[inline]
317 pub fn set_content_type(mut self, mime_type: Mime) -> Self {
318 self.content_type = mime_type;
319 self
320 }
321
322 #[inline]
330 pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self {
331 self.content_disposition = cd;
332 self.flags.insert(Flags::CONTENT_DISPOSITION);
333 self
334 }
335
336 #[inline]
340 pub fn disable_content_disposition(mut self) -> Self {
341 self.flags.remove(Flags::CONTENT_DISPOSITION);
342 self
343 }
344
345 #[inline]
351 pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
352 self.encoding = Some(enc);
353 self
354 }
355
356 #[inline]
360 pub fn use_etag(mut self, value: bool) -> Self {
361 self.flags.set(Flags::ETAG, value);
362 self
363 }
364
365 #[inline]
369 pub fn use_last_modified(mut self, value: bool) -> Self {
370 self.flags.set(Flags::LAST_MD, value);
371 self
372 }
373
374 #[inline]
378 pub fn prefer_utf8(mut self, value: bool) -> Self {
379 self.flags.set(Flags::PREFER_UTF8, value);
380 self
381 }
382
383 pub(crate) fn etag(&self) -> Option<header::EntityTag> {
385 self.modified.as_ref().map(|mtime| {
386 let ino = {
387 #[cfg(unix)]
388 {
389 #[cfg(unix)]
390 use std::os::unix::fs::MetadataExt as _;
391
392 self.md.ino()
393 }
394
395 #[cfg(not(unix))]
396 {
397 0
398 }
399 };
400
401 let dur = mtime
402 .duration_since(UNIX_EPOCH)
403 .expect("modification time must be after epoch");
404
405 header::EntityTag::new_strong(format!(
406 "{:x}:{:x}:{:x}:{:x}",
407 ino,
408 self.md.len(),
409 dur.as_secs(),
410 dur.subsec_nanos()
411 ))
412 })
413 }
414
415 pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
416 self.modified.map(|mtime| mtime.into())
417 }
418
419 pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
421 if self.status_code != StatusCode::OK {
422 let mut res = HttpResponse::build(self.status_code);
423
424 let ct = if self.flags.contains(Flags::PREFER_UTF8) {
425 equiv_utf8_text(self.content_type.clone())
426 } else {
427 self.content_type
428 };
429
430 res.insert_header((header::CONTENT_TYPE, ct.to_string()));
431
432 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
433 res.insert_header((
434 header::CONTENT_DISPOSITION,
435 self.content_disposition.to_string(),
436 ));
437 }
438
439 if let Some(current_encoding) = self.encoding {
440 res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
441 }
442
443 let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
444
445 return res.streaming(reader);
446 }
447
448 let etag = if self.flags.contains(Flags::ETAG) {
449 self.etag()
450 } else {
451 None
452 };
453
454 let last_modified = if self.flags.contains(Flags::LAST_MD) {
455 self.last_modified()
456 } else {
457 None
458 };
459
460 let precondition_failed = if !any_match(etag.as_ref(), req) {
462 true
463 } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
464 (last_modified, req.get_header())
465 {
466 let t1: SystemTime = (*m).into();
467 let t2: SystemTime = (*since).into();
468
469 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
470 (Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
471 _ => false,
472 }
473 } else {
474 false
475 };
476
477 let not_modified = if !none_match(etag.as_ref(), req) {
479 true
480 } else if req.headers().contains_key(header::IF_NONE_MATCH) {
481 false
482 } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
483 (last_modified, req.get_header())
484 {
485 let t1: SystemTime = (*m).into();
486 let t2: SystemTime = (*since).into();
487
488 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
489 (Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
490 _ => false,
491 }
492 } else {
493 false
494 };
495
496 let mut res = HttpResponse::build(self.status_code);
497
498 let ct = if self.flags.contains(Flags::PREFER_UTF8) {
499 equiv_utf8_text(self.content_type.clone())
500 } else {
501 self.content_type
502 };
503
504 res.insert_header((header::CONTENT_TYPE, ct.to_string()));
505
506 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
507 res.insert_header((
508 header::CONTENT_DISPOSITION,
509 self.content_disposition.to_string(),
510 ));
511 }
512
513 if let Some(current_encoding) = self.encoding {
514 res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
515 }
516
517 if let Some(lm) = last_modified {
518 res.insert_header((header::LAST_MODIFIED, lm.to_string()));
519 }
520
521 if let Some(etag) = etag {
522 res.insert_header((header::ETAG, etag.to_string()));
523 }
524
525 res.insert_header((header::ACCEPT_RANGES, "bytes"));
526
527 let mut length = self.md.len();
528 let mut offset = 0;
529
530 if let Some(ranges) = req.headers().get(header::RANGE) {
532 if let Ok(ranges_header) = ranges.to_str() {
533 if let Ok(ranges) = HttpRange::parse(ranges_header, length) {
534 length = ranges[0].length;
535 offset = ranges[0].start;
536
537 if req.headers().contains_key(&header::ACCEPT_ENCODING) {
551 res.insert_header((
553 header::CONTENT_ENCODING,
554 HeaderValue::from_static("identity"),
555 ));
556 }
557
558 res.insert_header((
559 header::CONTENT_RANGE,
560 format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
561 ));
562 } else {
563 res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
564 return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
565 };
566 } else {
567 return res.status(StatusCode::BAD_REQUEST).finish();
568 };
569 };
570
571 if precondition_failed {
572 return res.status(StatusCode::PRECONDITION_FAILED).finish();
573 } else if not_modified {
574 return res
575 .status(StatusCode::NOT_MODIFIED)
576 .body(body::None::new())
577 .map_into_boxed_body();
578 }
579
580 let reader = chunked::new_chunked_read(length, offset, self.file);
581
582 if offset != 0 || length != self.md.len() {
583 res.status(StatusCode::PARTIAL_CONTENT);
584 }
585
586 res.body(SizedStream::new(length, reader))
587 }
588}
589
590fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
592 match req.get_header::<header::IfMatch>() {
593 None | Some(header::IfMatch::Any) => true,
594
595 Some(header::IfMatch::Items(ref items)) => {
596 if let Some(some_etag) = etag {
597 for item in items {
598 if item.strong_eq(some_etag) {
599 return true;
600 }
601 }
602 }
603
604 false
605 }
606 }
607}
608
609fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
611 match req.get_header::<header::IfNoneMatch>() {
612 Some(header::IfNoneMatch::Any) => false,
613
614 Some(header::IfNoneMatch::Items(ref items)) => {
615 if let Some(some_etag) = etag {
616 for item in items {
617 if item.weak_eq(some_etag) {
618 return false;
619 }
620 }
621 }
622
623 true
624 }
625
626 None => true,
627 }
628}
629
630impl Responder for NamedFile {
631 type Body = BoxBody;
632
633 fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
634 self.into_response(req)
635 }
636}
637
638impl ServiceFactory<ServiceRequest> for NamedFile {
639 type Response = ServiceResponse;
640 type Error = Error;
641 type Config = ();
642 type Service = NamedFileService;
643 type InitError = ();
644 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
645
646 fn new_service(&self, _: ()) -> Self::Future {
647 let service = NamedFileService {
648 path: self.path.clone(),
649 };
650
651 Box::pin(async move { Ok(service) })
652 }
653}
654
655#[doc(hidden)]
656#[derive(Debug)]
657pub struct NamedFileService {
658 path: PathBuf,
659}
660
661impl Service<ServiceRequest> for NamedFileService {
662 type Response = ServiceResponse;
663 type Error = Error;
664 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
665
666 dev::always_ready!();
667
668 fn call(&self, req: ServiceRequest) -> Self::Future {
669 let (req, _) = req.into_parts();
670
671 let path = self.path.clone();
672 Box::pin(async move {
673 let file = NamedFile::open_async(path).await?;
674 let res = file.into_response(&req);
675 Ok(ServiceResponse::new(req, res))
676 })
677 }
678}
679
680impl HttpServiceFactory for NamedFile {
681 fn register(self, config: &mut AppService) {
682 config.register_service(
683 ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
684 None,
685 self,
686 None,
687 )
688 }
689}