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 pub(crate) read_mode_threshold: u64,
84}
85
86#[cfg(not(feature = "experimental-io-uring"))]
87pub(crate) use std::fs::File;
88
89#[cfg(feature = "experimental-io-uring")]
90pub(crate) use tokio_uring::fs::File;
91
92use super::chunked;
93
94impl NamedFile {
95 pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
116 let path = path.as_ref().to_path_buf();
117
118 let (content_type, content_disposition) = {
121 let filename = match path.file_name() {
122 Some(name) => name.to_string_lossy(),
123 None => {
124 return Err(io::Error::new(
125 io::ErrorKind::InvalidInput,
126 "Provided path has no filename",
127 ));
128 }
129 };
130
131 let ct = mime_guess::from_path(&path).first_or_octet_stream();
132
133 let disposition = match ct.type_() {
134 mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
135 mime::APPLICATION => match ct.subtype() {
136 mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
137 name if name == "wasm" || name == "xhtml" => DispositionType::Inline,
138 _ => DispositionType::Attachment,
139 },
140 _ => DispositionType::Attachment,
141 };
142
143 let filename_s = filename
145 .replace('\n', "%0A") .replace('\x0B', "%0B") .replace('\x0C', "%0C") .replace('\r', "%0D"); let mut parameters = vec![DispositionParam::Filename(filename_s)];
150
151 if !filename.is_ascii() {
152 parameters.push(DispositionParam::FilenameExt(ExtendedValue {
153 charset: Charset::Ext(String::from("UTF-8")),
154 language_tag: None,
155 value: filename.into_owned().into_bytes(),
156 }))
157 }
158
159 let cd = ContentDisposition {
160 disposition,
161 parameters,
162 };
163
164 (ct, cd)
165 };
166
167 let md = {
168 #[cfg(not(feature = "experimental-io-uring"))]
169 {
170 file.metadata()?
171 }
172
173 #[cfg(feature = "experimental-io-uring")]
174 {
175 use std::os::unix::prelude::{AsRawFd, FromRawFd};
176
177 let fd = file.as_raw_fd();
178
179 unsafe {
181 let file = std::fs::File::from_raw_fd(fd);
182 let md = file.metadata();
183 std::mem::forget(file);
186 md?
187 }
188 }
189 };
190
191 let modified = md.modified().ok();
192 let encoding = None;
193
194 Ok(NamedFile {
195 path,
196 file,
197 content_type,
198 content_disposition,
199 md,
200 modified,
201 encoding,
202 status_code: StatusCode::OK,
203 flags: Flags::default(),
204 read_mode_threshold: 0,
205 })
206 }
207
208 #[cfg(not(feature = "experimental-io-uring"))]
216 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
217 let file = File::open(&path)?;
218 Self::from_file(file, path)
219 }
220
221 pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
234 let file = {
235 #[cfg(not(feature = "experimental-io-uring"))]
236 {
237 File::open(&path)?
238 }
239
240 #[cfg(feature = "experimental-io-uring")]
241 {
242 File::open(&path).await?
243 }
244 };
245
246 Self::from_file(file, path)
247 }
248
249 #[inline]
251 pub fn file(&self) -> &File {
252 &self.file
253 }
254
255 #[inline]
269 pub fn path(&self) -> &Path {
270 self.path.as_path()
271 }
272
273 #[inline]
278 pub fn modified(&self) -> Option<SystemTime> {
279 self.modified
280 }
281
282 #[inline]
284 pub fn metadata(&self) -> &Metadata {
285 &self.md
286 }
287
288 #[inline]
290 pub fn content_type(&self) -> &Mime {
291 &self.content_type
292 }
293
294 #[inline]
296 pub fn content_disposition(&self) -> &ContentDisposition {
297 &self.content_disposition
298 }
299
300 #[inline]
305 pub fn content_encoding(&self) -> Option<ContentEncoding> {
306 self.encoding
307 }
308
309 #[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")]
311 pub fn set_status_code(mut self, status: StatusCode) -> Self {
312 self.status_code = status;
313 self
314 }
315
316 #[inline]
319 pub fn set_content_type(mut self, mime_type: Mime) -> Self {
320 self.content_type = mime_type;
321 self
322 }
323
324 #[inline]
332 pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self {
333 self.content_disposition = cd;
334 self.flags.insert(Flags::CONTENT_DISPOSITION);
335 self
336 }
337
338 #[inline]
342 pub fn disable_content_disposition(mut self) -> Self {
343 self.flags.remove(Flags::CONTENT_DISPOSITION);
344 self
345 }
346
347 #[inline]
353 pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
354 self.encoding = Some(enc);
355 self
356 }
357
358 pub fn read_mode_threshold(mut self, size: u64) -> Self {
371 self.read_mode_threshold = size;
372 self
373 }
374
375 #[inline]
379 pub fn use_etag(mut self, value: bool) -> Self {
380 self.flags.set(Flags::ETAG, value);
381 self
382 }
383
384 #[inline]
388 pub fn use_last_modified(mut self, value: bool) -> Self {
389 self.flags.set(Flags::LAST_MD, value);
390 self
391 }
392
393 #[inline]
397 pub fn prefer_utf8(mut self, value: bool) -> Self {
398 self.flags.set(Flags::PREFER_UTF8, value);
399 self
400 }
401
402 pub(crate) fn etag(&self) -> Option<header::EntityTag> {
404 self.modified.as_ref().map(|mtime| {
405 let ino = {
406 #[cfg(unix)]
407 {
408 #[cfg(unix)]
409 use std::os::unix::fs::MetadataExt as _;
410
411 self.md.ino()
412 }
413
414 #[cfg(not(unix))]
415 {
416 0
417 }
418 };
419
420 let dur = mtime
421 .duration_since(UNIX_EPOCH)
422 .expect("modification time must be after epoch");
423
424 header::EntityTag::new_strong(format!(
425 "{:x}:{:x}:{:x}:{:x}",
426 ino,
427 self.md.len(),
428 dur.as_secs(),
429 dur.subsec_nanos()
430 ))
431 })
432 }
433
434 pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
435 self.modified.map(|mtime| mtime.into())
436 }
437
438 pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
440 if self.status_code != StatusCode::OK {
441 let mut res = HttpResponse::build(self.status_code);
442
443 let ct = if self.flags.contains(Flags::PREFER_UTF8) {
444 equiv_utf8_text(self.content_type.clone())
445 } else {
446 self.content_type
447 };
448
449 res.insert_header((header::CONTENT_TYPE, ct.to_string()));
450
451 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
452 res.insert_header((
453 header::CONTENT_DISPOSITION,
454 self.content_disposition.to_string(),
455 ));
456 }
457
458 if let Some(current_encoding) = self.encoding {
459 res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
460 }
461
462 let reader =
463 chunked::new_chunked_read(self.md.len(), 0, self.file, self.read_mode_threshold);
464
465 return res.streaming(reader);
466 }
467
468 let etag = if self.flags.contains(Flags::ETAG) {
469 self.etag()
470 } else {
471 None
472 };
473
474 let last_modified = if self.flags.contains(Flags::LAST_MD) {
475 self.last_modified()
476 } else {
477 None
478 };
479
480 let precondition_failed = if !any_match(etag.as_ref(), req) {
482 true
483 } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
484 (last_modified, req.get_header())
485 {
486 let t1: SystemTime = (*m).into();
487 let t2: SystemTime = (*since).into();
488
489 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
490 (Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
491 _ => false,
492 }
493 } else {
494 false
495 };
496
497 let not_modified = if !none_match(etag.as_ref(), req) {
499 true
500 } else if req.headers().contains_key(header::IF_NONE_MATCH) {
501 false
502 } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
503 (last_modified, req.get_header())
504 {
505 let t1: SystemTime = (*m).into();
506 let t2: SystemTime = (*since).into();
507
508 match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
509 (Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
510 _ => false,
511 }
512 } else {
513 false
514 };
515
516 let mut res = HttpResponse::build(self.status_code);
517
518 let ct = if self.flags.contains(Flags::PREFER_UTF8) {
519 equiv_utf8_text(self.content_type.clone())
520 } else {
521 self.content_type
522 };
523
524 res.insert_header((header::CONTENT_TYPE, ct.to_string()));
525
526 if self.flags.contains(Flags::CONTENT_DISPOSITION) {
527 res.insert_header((
528 header::CONTENT_DISPOSITION,
529 self.content_disposition.to_string(),
530 ));
531 }
532
533 if let Some(current_encoding) = self.encoding {
534 res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
535 }
536
537 if let Some(lm) = last_modified {
538 res.insert_header((header::LAST_MODIFIED, lm.to_string()));
539 }
540
541 if let Some(etag) = etag {
542 res.insert_header((header::ETAG, etag.to_string()));
543 }
544
545 res.insert_header((header::ACCEPT_RANGES, "bytes"));
546
547 let mut length = self.md.len();
548 let mut offset = 0;
549
550 if let Some(ranges) = req.headers().get(header::RANGE) {
552 if let Ok(ranges_header) = ranges.to_str() {
553 if let Ok(ranges) = HttpRange::parse(ranges_header, length) {
554 length = ranges[0].length;
555 offset = ranges[0].start;
556
557 if req.headers().contains_key(&header::ACCEPT_ENCODING) {
571 res.insert_header((
573 header::CONTENT_ENCODING,
574 HeaderValue::from_static("identity"),
575 ));
576 }
577
578 res.insert_header((
579 header::CONTENT_RANGE,
580 format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
581 ));
582 } else {
583 res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
584 return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
585 };
586 } else {
587 return res.status(StatusCode::BAD_REQUEST).finish();
588 };
589 };
590
591 if precondition_failed {
592 return res.status(StatusCode::PRECONDITION_FAILED).finish();
593 } else if not_modified {
594 return res
595 .status(StatusCode::NOT_MODIFIED)
596 .body(body::None::new())
597 .map_into_boxed_body();
598 }
599
600 let reader = chunked::new_chunked_read(length, offset, self.file, self.read_mode_threshold);
601
602 if offset != 0 || length != self.md.len() {
603 res.status(StatusCode::PARTIAL_CONTENT);
604 }
605
606 res.body(SizedStream::new(length, reader))
607 }
608}
609
610fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
612 match req.get_header::<header::IfMatch>() {
613 None | Some(header::IfMatch::Any) => true,
614
615 Some(header::IfMatch::Items(ref items)) => {
616 if let Some(some_etag) = etag {
617 for item in items {
618 if item.strong_eq(some_etag) {
619 return true;
620 }
621 }
622 }
623
624 false
625 }
626 }
627}
628
629fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
631 match req.get_header::<header::IfNoneMatch>() {
632 Some(header::IfNoneMatch::Any) => false,
633
634 Some(header::IfNoneMatch::Items(ref items)) => {
635 if let Some(some_etag) = etag {
636 for item in items {
637 if item.weak_eq(some_etag) {
638 return false;
639 }
640 }
641 }
642
643 true
644 }
645
646 None => true,
647 }
648}
649
650impl Responder for NamedFile {
651 type Body = BoxBody;
652
653 fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
654 self.into_response(req)
655 }
656}
657
658impl ServiceFactory<ServiceRequest> for NamedFile {
659 type Response = ServiceResponse;
660 type Error = Error;
661 type Config = ();
662 type Service = NamedFileService;
663 type InitError = ();
664 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
665
666 fn new_service(&self, _: ()) -> Self::Future {
667 let service = NamedFileService {
668 path: self.path.clone(),
669 };
670
671 Box::pin(async move { Ok(service) })
672 }
673}
674
675#[doc(hidden)]
676#[derive(Debug)]
677pub struct NamedFileService {
678 path: PathBuf,
679}
680
681impl Service<ServiceRequest> for NamedFileService {
682 type Response = ServiceResponse;
683 type Error = Error;
684 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
685
686 dev::always_ready!();
687
688 fn call(&self, req: ServiceRequest) -> Self::Future {
689 let (req, _) = req.into_parts();
690
691 let path = self.path.clone();
692 Box::pin(async move {
693 let file = NamedFile::open_async(path).await?;
694 let res = file.into_response(&req);
695 Ok(ServiceResponse::new(req, res))
696 })
697 }
698}
699
700impl HttpServiceFactory for NamedFile {
701 fn register(self, config: &mut AppService) {
702 config.register_service(
703 ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
704 None,
705 self,
706 None,
707 )
708 }
709}