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}