ntex_files/
lib.rs

1#![allow(
2    type_alias_bounds,
3    clippy::borrow_interior_mutable_const,
4    clippy::type_complexity,
5    clippy::result_unit_err,
6    clippy::enum_variant_names
7)]
8
9//! Static files support
10use std::fs::{DirEntry, File};
11use std::path::{Path, PathBuf};
12use std::{
13    cmp, fmt::Write, io, io::Read, io::Seek, pin::Pin, rc::Rc, task::Context, task::Poll,
14};
15
16use futures::future::{FutureExt, LocalBoxFuture};
17use futures::{Future, Stream};
18use mime_guess::from_ext;
19use ntex::http::error::BlockingError;
20use ntex::http::{Method, Payload, Uri, header};
21use ntex::router::{ResourceDef, ResourcePath};
22use ntex::service::boxed::{self, BoxService, BoxServiceFactory};
23use ntex::service::{IntoServiceFactory, Service, ServiceCtx, ServiceFactory};
24use ntex::web::dev::{WebServiceConfig, WebServiceFactory};
25use ntex::web::error::ErrorRenderer;
26use ntex::web::guard::Guard;
27use ntex::web::{self, FromRequest, HttpRequest, HttpResponse, WebRequest, WebResponse};
28use ntex::{SharedCfg, util::Bytes};
29use percent_encoding::{CONTROLS, utf8_percent_encode};
30use v_htmlescape::escape as escape_html_entity;
31
32mod error;
33mod file_header;
34mod named;
35mod range;
36
37use self::error::{FilesError, UriSegmentError};
38pub use crate::named::NamedFile;
39pub use crate::range::HttpRange;
40
41type HttpService<Err: ErrorRenderer> = BoxService<WebRequest<Err>, WebResponse, Err::Container>;
42type HttpServiceFactory<Err: ErrorRenderer> =
43    BoxServiceFactory<SharedCfg, WebRequest<Err>, WebResponse, Err::Container, ()>;
44
45/// Return the MIME type associated with a filename extension (case-insensitive).
46/// If `ext` is empty or no associated type for the extension was found, returns
47/// the type `application/octet-stream`.
48#[inline]
49pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
50    from_ext(ext).first_or_octet_stream()
51}
52
53#[doc(hidden)]
54/// A helper created from a `std::fs::File` which reads the file
55/// chunk-by-chunk on a `ThreadPool`.
56pub struct ChunkedReadFile {
57    size: u64,
58    offset: u64,
59    file: Option<File>,
60    fut: Option<LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>>,
61    counter: u64,
62}
63
64impl Stream for ChunkedReadFile {
65    type Item = Result<Bytes, std::io::Error>;
66
67    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
68        if let Some(ref mut fut) = self.fut {
69            return match Pin::new(fut).poll(cx) {
70                Poll::Ready(Ok((file, bytes))) => {
71                    self.fut.take();
72                    self.file = Some(file);
73                    self.offset += bytes.len() as u64;
74                    self.counter += bytes.len() as u64;
75                    Poll::Ready(Some(Ok(bytes)))
76                }
77                Poll::Ready(Err(e)) => {
78                    let e = match e {
79                        BlockingError::Error(e) => e,
80                        BlockingError::Canceled => io::Error::other("Operation is canceled"),
81                    };
82                    Poll::Ready(Some(Err(e)))
83                }
84                Poll::Pending => Poll::Pending,
85            };
86        }
87
88        let size = self.size;
89        let offset = self.offset;
90        let counter = self.counter;
91
92        if size == counter {
93            Poll::Ready(None)
94        } else {
95            let mut file = self.file.take().expect("Use after completion");
96            self.fut = Some(
97                web::block(move || {
98                    let max_bytes: usize =
99                        cmp::min(size.saturating_sub(counter), 65_536) as usize;
100                    let mut buf = Vec::with_capacity(max_bytes);
101                    file.seek(io::SeekFrom::Start(offset))?;
102                    let nbytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
103                    if nbytes == 0 {
104                        return Err(io::ErrorKind::UnexpectedEof.into());
105                    }
106                    Ok((file, Bytes::from(buf)))
107                })
108                .boxed_local(),
109            );
110            self.poll_next(cx)
111        }
112    }
113}
114
115type DirectoryRenderer = dyn Fn(&Directory, &HttpRequest) -> Result<WebResponse, io::Error>;
116
117/// A directory; responds with the generated directory listing.
118#[derive(Debug)]
119pub struct Directory {
120    /// Base directory
121    pub base: PathBuf,
122    /// Path of subdirectory to generate listing for
123    pub path: PathBuf,
124}
125
126impl Directory {
127    /// Create a new directory
128    pub fn new(base: PathBuf, path: PathBuf) -> Directory {
129        Directory { base, path }
130    }
131
132    /// Is this entry visible from this directory?
133    pub fn is_visible(&self, entry: &io::Result<DirEntry>) -> bool {
134        if let Ok(ref entry) = *entry {
135            if let Some(name) = entry.file_name().to_str() {
136                if name.starts_with('.') {
137                    return false;
138                }
139            }
140            if let Ok(ref md) = entry.metadata() {
141                let ft = md.file_type();
142                return ft.is_dir() || ft.is_file() || ft.is_symlink();
143            }
144        }
145        false
146    }
147}
148
149// show file url as relative to static path
150macro_rules! encode_file_url {
151    ($path:ident) => {
152        utf8_percent_encode(&$path, CONTROLS)
153    };
154}
155
156// " -- &quot;  & -- &amp;  ' -- &#x27;  < -- &lt;  > -- &gt;  / -- &#x2f;
157macro_rules! encode_file_name {
158    ($entry:ident) => {
159        escape_html_entity(&$entry.file_name().to_string_lossy())
160    };
161}
162
163fn directory_listing(dir: &Directory, req: &HttpRequest) -> Result<WebResponse, io::Error> {
164    let index_of = format!("Index of {}", req.path());
165    let mut body = String::new();
166    let base = Path::new(req.path());
167
168    for entry in dir.path.read_dir()? {
169        if dir.is_visible(&entry) {
170            let entry = entry.unwrap();
171            let p = match entry.path().strip_prefix(&dir.path) {
172                Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace('\\', "/"),
173                Ok(p) => base.join(p).to_string_lossy().into_owned(),
174                Err(_) => continue,
175            };
176
177            // if file is a directory, add '/' to the end of the name
178            if let Ok(metadata) = entry.metadata() {
179                if metadata.is_dir() {
180                    let _ = write!(
181                        body,
182                        "<li><a href=\"{}\">{}/</a></li>",
183                        encode_file_url!(p),
184                        encode_file_name!(entry),
185                    );
186                } else {
187                    let _ = write!(
188                        body,
189                        "<li><a href=\"{}\">{}</a></li>",
190                        encode_file_url!(p),
191                        encode_file_name!(entry),
192                    );
193                }
194            } else {
195                continue;
196            }
197        }
198    }
199
200    let html = format!(
201        "<html>\
202         <head><title>{}</title></head>\
203         <body><h1>{}</h1>\
204         <ul>\
205         {}\
206         </ul></body>\n</html>",
207        index_of, index_of, body
208    );
209    Ok(WebResponse::new(
210        HttpResponse::Ok().content_type("text/html; charset=utf-8").body(html),
211        req.clone(),
212    ))
213}
214
215type MimeOverride = dyn Fn(&mime::Name) -> file_header::DispositionType;
216
217/// Static files handling
218///
219/// `Files` service must be registered with `App::service()` method.
220///
221/// ```rust
222/// use ntex::web::App;
223/// use ntex_files as fs;
224///
225/// let app = App::new()
226///    .service(fs::Files::new("/static", "."));
227/// ```
228pub struct Files<Err: ErrorRenderer> {
229    path: String,
230    directory: PathBuf,
231    index: Option<String>,
232    show_index: bool,
233    redirect_to_slash: bool,
234    default: Option<Rc<HttpServiceFactory<Err>>>,
235    renderer: Rc<DirectoryRenderer>,
236    mime_override: Option<Rc<MimeOverride>>,
237    file_flags: named::Flags,
238    guards: Option<Rc<dyn Guard>>,
239}
240
241impl<Err: ErrorRenderer> Clone for Files<Err> {
242    fn clone(&self) -> Self {
243        Self {
244            directory: self.directory.clone(),
245            index: self.index.clone(),
246            show_index: self.show_index,
247            redirect_to_slash: self.redirect_to_slash,
248            default: self.default.clone(),
249            renderer: self.renderer.clone(),
250            file_flags: self.file_flags.clone(),
251            path: self.path.clone(),
252            mime_override: self.mime_override.clone(),
253            guards: self.guards.clone(),
254        }
255    }
256}
257
258impl<Err: ErrorRenderer> Files<Err> {
259    /// Create new `Files` instance for specified base directory.
260    ///
261    /// `File` uses `ThreadPool` for blocking filesystem operations.
262    /// By default pool with 5x threads of available cpus is used.
263    /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable.
264    pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Self {
265        let orig_dir = dir.into();
266        let dir = match orig_dir.canonicalize() {
267            Ok(canon_dir) => canon_dir,
268            Err(_) => {
269                log::error!("Specified path is not a directory: {:?}", orig_dir);
270                PathBuf::new()
271            }
272        };
273
274        Files {
275            path: path.to_string(),
276            directory: dir,
277            index: None,
278            show_index: false,
279            redirect_to_slash: false,
280            default: None,
281            renderer: Rc::new(directory_listing),
282            mime_override: None,
283            file_flags: named::Flags::default(),
284            guards: None,
285        }
286    }
287
288    /// Show files listing for directories.
289    ///
290    /// By default show files listing is disabled.
291    pub fn show_files_listing(mut self) -> Self {
292        self.show_index = true;
293        self
294    }
295
296    /// Redirects to a slash-ended path when browsing a directory.
297    ///
298    /// By default never redirect.
299    pub fn redirect_to_slash_directory(mut self) -> Self {
300        self.redirect_to_slash = true;
301        self
302    }
303
304    /// Set custom directory renderer
305    pub fn files_listing_renderer<F>(mut self, f: F) -> Self
306    where
307        for<'r, 's> F:
308            Fn(&'r Directory, &'s HttpRequest) -> Result<WebResponse, io::Error> + 'static,
309    {
310        self.renderer = Rc::new(f);
311        self
312    }
313
314    /// Specifies mime override callback
315    pub fn mime_override<F>(mut self, f: F) -> Self
316    where
317        F: Fn(&mime::Name) -> file_header::DispositionType + 'static,
318    {
319        self.mime_override = Some(Rc::new(f));
320        self
321    }
322
323    /// Set index file
324    ///
325    /// Shows specific index file for directory "/" instead of
326    /// showing files listing.
327    pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
328        self.index = Some(index.into());
329        self
330    }
331
332    #[inline]
333    /// Specifies whether to use ETag or not.
334    ///
335    /// Default is true.
336    pub fn use_etag(mut self, value: bool) -> Self {
337        self.file_flags.set(named::Flags::ETAG, value);
338        self
339    }
340
341    #[inline]
342    /// Specifies whether to use Last-Modified or not.
343    ///
344    /// Default is true.
345    pub fn use_last_modified(mut self, value: bool) -> Self {
346        self.file_flags.set(named::Flags::LAST_MD, value);
347        self
348    }
349
350    /// Specifies custom guards to use for directory listings and files.
351    ///
352    /// Default behaviour allows GET and HEAD.
353    #[inline]
354    pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
355        self.guards = Some(Rc::new(guards));
356        self
357    }
358
359    /// Disable `Content-Disposition` header.
360    ///
361    /// By default Content-Disposition` header is enabled.
362    #[inline]
363    pub fn disable_content_disposition(mut self) -> Self {
364        self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
365        self
366    }
367
368    /// Sets default handler which is used when no matched file could be found.
369    pub fn default_handler<F, U>(mut self, f: F) -> Self
370    where
371        F: IntoServiceFactory<U, WebRequest<Err>, SharedCfg>,
372        U: ServiceFactory<
373                WebRequest<Err>,
374                SharedCfg,
375                Response = WebResponse,
376                Error = Err::Container,
377            > + 'static,
378    {
379        // create and configure default resource
380        self.default = Some(Rc::new(boxed::factory(f.into_factory().map_init_err(|_| ()))));
381
382        self
383    }
384}
385
386impl<Err> WebServiceFactory<Err> for Files<Err>
387where
388    Err: ErrorRenderer,
389    Err::Container: From<FilesError>,
390{
391    fn register(mut self, config: &mut WebServiceConfig<Err>) {
392        if self.default.is_none() {
393            self.default = Some(config.default_service());
394        }
395        let rdef = if config.is_root() {
396            ResourceDef::root_prefix(&self.path)
397        } else {
398            ResourceDef::prefix(&self.path)
399        };
400        config.register_service(rdef, None, self, None)
401    }
402}
403
404impl<Err> ServiceFactory<WebRequest<Err>, SharedCfg> for Files<Err>
405where
406    Err: ErrorRenderer,
407    Err::Container: From<FilesError>,
408{
409    type Response = WebResponse;
410    type Error = Err::Container;
411    type Service = FilesService<Err>;
412    type InitError = ();
413
414    async fn create(&self, cfg: SharedCfg) -> Result<Self::Service, Self::InitError> {
415        let mut srv = FilesService {
416            directory: self.directory.clone(),
417            index: self.index.clone(),
418            show_index: self.show_index,
419            redirect_to_slash: self.redirect_to_slash,
420            default: None,
421            renderer: self.renderer.clone(),
422            mime_override: self.mime_override.clone(),
423            file_flags: self.file_flags.clone(),
424            guards: self.guards.clone(),
425        };
426
427        if let Some(default) = self.default.as_ref() {
428            default
429                .create(cfg)
430                .map(move |result| match result {
431                    Ok(default) => {
432                        srv.default = Some(default);
433                        Ok(srv)
434                    }
435                    Err(_) => Err(()),
436                })
437                .await
438        } else {
439            Ok(srv)
440        }
441    }
442}
443
444pub struct FilesService<Err: ErrorRenderer> {
445    directory: PathBuf,
446    index: Option<String>,
447    show_index: bool,
448    redirect_to_slash: bool,
449    default: Option<HttpService<Err>>,
450    renderer: Rc<DirectoryRenderer>,
451    mime_override: Option<Rc<MimeOverride>>,
452    file_flags: named::Flags,
453    guards: Option<Rc<dyn Guard>>,
454}
455
456impl<Err: ErrorRenderer> FilesService<Err>
457where
458    Err::Container: From<FilesError>,
459{
460    async fn handle_io_error(
461        &self,
462        e: io::Error,
463        req: WebRequest<Err>,
464        ctx: ServiceCtx<'_, Self>,
465    ) -> Result<WebResponse, Err::Container> {
466        log::debug!("Files: Failed to handle {}: {}", req.path(), e);
467        if let Some(ref default) = self.default {
468            ctx.call(default, req).await
469        } else {
470            Ok(req.error_response(FilesError::from(e)))
471        }
472    }
473}
474
475impl<Err> Service<WebRequest<Err>> for FilesService<Err>
476where
477    Err: ErrorRenderer,
478    Err::Container: From<FilesError>,
479{
480    type Response = WebResponse;
481    type Error = Err::Container;
482
483    async fn call(
484        &self,
485        req: WebRequest<Err>,
486        ctx: ServiceCtx<'_, Self>,
487    ) -> Result<Self::Response, Self::Error> {
488        let is_method_valid = if let Some(guard) = &self.guards {
489            // execute user defined guards
490            (**guard).check(req.head())
491        } else {
492            // default behaviour
493            matches!(*req.method(), Method::HEAD | Method::GET)
494        };
495
496        if !is_method_valid {
497            return Ok(req.error_response(FilesError::MethodNotAllowed));
498        }
499
500        let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) {
501            Ok(item) => item,
502            Err(e) => return Ok(req.error_response(FilesError::from(e))),
503        };
504
505        // full filepath
506        let path = match self.directory.join(real_path.0).canonicalize() {
507            Ok(path) => path,
508            Err(e) => return self.handle_io_error(e, req, ctx).await,
509        };
510
511        if path.is_dir() {
512            if let Some(ref redir_index) = self.index {
513                if self.redirect_to_slash && !req.path().ends_with('/') {
514                    let redirect_to = format!("{}/", req.path());
515                    return Ok(req.into_response(
516                        HttpResponse::Found()
517                            .header(header::LOCATION, redirect_to)
518                            .body("")
519                            .into_body(),
520                    ));
521                }
522
523                let path = path.join(redir_index);
524
525                match NamedFile::open(path) {
526                    Ok(mut named_file) => {
527                        if let Some(ref mime_override) = self.mime_override {
528                            let new_disposition =
529                                mime_override(&named_file.content_type.type_());
530                            named_file.content_disposition.disposition = new_disposition;
531                        }
532
533                        named_file.flags = self.file_flags.clone();
534                        let (req, _) = req.into_parts();
535                        Ok(WebResponse::new(named_file.into_response(&req), req))
536                    }
537                    Err(e) => self.handle_io_error(e, req, ctx).await,
538                }
539            } else if self.show_index {
540                let dir = Directory::new(self.directory.clone(), path);
541                let (req, _) = req.into_parts();
542                let x = (self.renderer)(&dir, &req);
543                match x {
544                    Ok(resp) => Ok(resp),
545                    Err(e) => Ok(WebResponse::from_err::<Err, _>(FilesError::from(e), req)),
546                }
547            } else {
548                Ok(WebResponse::from_err::<Err, _>(FilesError::IsDirectory, req.into_parts().0))
549            }
550        } else {
551            match NamedFile::open(path) {
552                Ok(mut named_file) => {
553                    if let Some(ref mime_override) = self.mime_override {
554                        let new_disposition = mime_override(&named_file.content_type.type_());
555                        named_file.content_disposition.disposition = new_disposition;
556                    }
557
558                    named_file.flags = self.file_flags.clone();
559                    let (req, _) = req.into_parts();
560                    Ok(WebResponse::new(named_file.into_response(&req), req))
561                }
562                Err(e) => self.handle_io_error(e, req, ctx).await,
563            }
564        }
565    }
566}
567
568#[derive(Debug)]
569struct PathBufWrp(PathBuf);
570
571impl PathBufWrp {
572    fn get_pathbuf(path: &str) -> Result<Self, UriSegmentError> {
573        let mut buf = PathBuf::new();
574        for segment in path.split('/') {
575            if segment == ".." {
576                buf.pop();
577            } else if segment.starts_with('.') {
578                return Err(UriSegmentError::BadStart('.'));
579            } else if segment.starts_with('*') {
580                return Err(UriSegmentError::BadStart('*'));
581            } else if segment.ends_with(':') {
582                return Err(UriSegmentError::BadEnd(':'));
583            } else if segment.ends_with('>') {
584                return Err(UriSegmentError::BadEnd('>'));
585            } else if segment.ends_with('<') {
586                return Err(UriSegmentError::BadEnd('<'));
587            } else if segment.is_empty() {
588                continue;
589            } else if cfg!(windows) && segment.contains('\\') {
590                return Err(UriSegmentError::BadChar('\\'));
591            } else {
592                buf.push(Uri::unquote(segment).as_ref())
593            }
594        }
595
596        Ok(PathBufWrp(buf))
597    }
598}
599
600impl<Err> FromRequest<Err> for PathBufWrp {
601    type Error = UriSegmentError;
602
603    async fn from_request(req: &HttpRequest, _: &mut Payload) -> Result<Self, Self::Error> {
604        PathBufWrp::get_pathbuf(req.match_info().path())
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use std::fs;
611    use std::iter::FromIterator;
612    use std::ops::Add;
613    use std::time::{Duration, SystemTime};
614
615    use super::*;
616    use ntex::http::{self, Method, StatusCode};
617    use ntex::web::middleware::Compress;
618    use ntex::web::test::{self, TestRequest};
619    use ntex::web::{App, DefaultError, guard};
620
621    #[ntex::test]
622    async fn test_file_extension_to_mime() {
623        let m = file_extension_to_mime("jpg");
624        assert_eq!(m, mime::IMAGE_JPEG);
625
626        let m = file_extension_to_mime("invalid extension!!");
627        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
628
629        let m = file_extension_to_mime("");
630        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
631    }
632
633    #[ntex::test]
634    async fn test_if_modified_since_without_if_none_match() {
635        let file = NamedFile::open("Cargo.toml").unwrap();
636        let since = file_header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
637
638        let req = TestRequest::default()
639            .header(http::header::IF_MODIFIED_SINCE, since.to_string())
640            .to_http_request();
641        let resp = test::respond_to(file, &req).await;
642        assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
643    }
644
645    #[ntex::test]
646    async fn test_if_modified_since_with_if_none_match() {
647        let file = NamedFile::open("Cargo.toml").unwrap();
648        let since = file_header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
649
650        let req = TestRequest::default()
651            .header(http::header::IF_NONE_MATCH, "miss_etag")
652            .header(http::header::IF_MODIFIED_SINCE, since.to_string())
653            .to_http_request();
654        let resp = test::respond_to(file, &req).await;
655        assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
656    }
657
658    #[ntex::test]
659    async fn test_named_file_text() {
660        assert!(NamedFile::open("test--").is_err());
661        let mut file = NamedFile::open("Cargo.toml").unwrap();
662        {
663            file.file();
664            let _f: &File = &file;
665        }
666        {
667            let _f: &mut File = &mut file;
668        }
669
670        let req = TestRequest::default().to_http_request();
671        let resp = test::respond_to(file, &req).await;
672        assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/x-toml");
673        assert_eq!(
674            resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
675            "inline; filename=\"Cargo.toml\""
676        );
677    }
678
679    #[ntex::test]
680    async fn test_named_file_content_disposition() {
681        assert!(NamedFile::open("test--").is_err());
682        let mut file = NamedFile::open("Cargo.toml").unwrap();
683        {
684            file.file();
685            let _f: &File = &file;
686        }
687        {
688            let _f: &mut File = &mut file;
689        }
690
691        let req = TestRequest::default().to_http_request();
692        let resp = test::respond_to(file, &req).await;
693        assert_eq!(
694            resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
695            "inline; filename=\"Cargo.toml\""
696        );
697
698        let file = NamedFile::open("Cargo.toml").unwrap().disable_content_disposition();
699        let req = TestRequest::default().to_http_request();
700        let resp = test::respond_to(file, &req).await;
701        assert!(resp.headers().get(http::header::CONTENT_DISPOSITION).is_none());
702    }
703
704    // #[ntex::test]
705    // async fn test_named_file_non_ascii_file_name() {
706    //     let mut file =
707    //         NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml")
708    //             .unwrap();
709    //     {
710    //         file.file();
711    //         let _f: &File = &file;
712    //     }
713    //     {
714    //         let _f: &mut File = &mut file;
715    //     }
716
717    //     let req = TestRequest::default().to_http_request();
718    //     let resp = test::respond_to(file, &req).await;
719    //     assert_eq!(
720    //         resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
721    //         "text/x-toml"
722    //     );
723    //     assert_eq!(
724    //         resp.headers()
725    //             .get(http::header::CONTENT_DISPOSITION)
726    //             .unwrap(),
727    //         "inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml"
728    //     );
729    // }
730
731    #[ntex::test]
732    async fn test_named_file_set_content_type() {
733        let mut file = NamedFile::open("Cargo.toml").unwrap().set_content_type(mime::TEXT_XML);
734        {
735            file.file();
736            let _f: &File = &file;
737        }
738        {
739            let _f: &mut File = &mut file;
740        }
741
742        let req = TestRequest::default().to_http_request();
743        let resp = test::respond_to(file, &req).await;
744        assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/xml");
745        assert_eq!(
746            resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
747            "inline; filename=\"Cargo.toml\""
748        );
749    }
750
751    #[ntex::test]
752    async fn test_named_file_image() {
753        let mut file = NamedFile::open("tests/test.png").unwrap();
754        {
755            file.file();
756            let _f: &File = &file;
757        }
758        {
759            let _f: &mut File = &mut file;
760        }
761
762        let req = TestRequest::default().to_http_request();
763        let resp = test::respond_to(file, &req).await;
764        assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "image/png");
765        assert_eq!(
766            resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
767            "inline; filename=\"test.png\""
768        );
769    }
770
771    #[ntex::test]
772    async fn test_named_file_image_attachment() {
773        use crate::file_header::{
774            Charset, ContentDisposition, DispositionParam, DispositionType,
775        };
776
777        let cd = ContentDisposition {
778            disposition: DispositionType::Attachment,
779            parameters: vec![DispositionParam::Filename(
780                Charset::Ext(String::from("UTF-8")),
781                None,
782                "test.png".to_string().into_bytes(),
783            )],
784        };
785        let mut file = NamedFile::open("tests/test.png").unwrap().set_content_disposition(cd);
786        {
787            file.file();
788            let _f: &File = &file;
789        }
790        {
791            let _f: &mut File = &mut file;
792        }
793
794        let req = TestRequest::default().to_http_request();
795        let resp = test::respond_to(file, &req).await;
796        assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "image/png");
797        assert_eq!(
798            resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
799            "attachment; filename=\"test.png\""
800        );
801    }
802
803    #[ntex::test]
804    async fn test_named_file_binary() {
805        let mut file = NamedFile::open("tests/test.binary").unwrap();
806        {
807            file.file();
808            let _f: &File = &file;
809        }
810        {
811            let _f: &mut File = &mut file;
812        }
813
814        let req = TestRequest::default().to_http_request();
815        let resp = test::respond_to(file, &req).await;
816        assert_eq!(
817            resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
818            "application/octet-stream"
819        );
820        assert_eq!(
821            resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
822            "attachment; filename=\"test.binary\""
823        );
824    }
825
826    #[ntex::test]
827    async fn test_named_file_status_code_text() {
828        let mut file =
829            NamedFile::open("Cargo.toml").unwrap().set_status_code(StatusCode::NOT_FOUND);
830        {
831            file.file();
832            let _f: &File = &file;
833        }
834        {
835            let _f: &mut File = &mut file;
836        }
837
838        let req = TestRequest::default().to_http_request();
839        let resp = test::respond_to(file, &req).await;
840        assert_eq!(resp.headers().get(http::header::CONTENT_TYPE).unwrap(), "text/x-toml");
841        assert_eq!(
842            resp.headers().get(http::header::CONTENT_DISPOSITION).unwrap(),
843            "inline; filename=\"Cargo.toml\""
844        );
845        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
846    }
847
848    #[ntex::test]
849    async fn test_mime_override() {
850        fn all_attachment(_: &mime::Name) -> file_header::DispositionType {
851            file_header::DispositionType::Attachment
852        }
853
854        let srv = test::init_service(App::new().service(
855            Files::new("/", ".").mime_override(all_attachment).index_file("Cargo.toml"),
856        ))
857        .await;
858
859        let request = TestRequest::get().uri("/").to_request();
860        let response = test::call_service(&srv, request).await;
861        assert_eq!(response.status(), StatusCode::OK);
862
863        let content_disposition = response
864            .headers()
865            .get(http::header::CONTENT_DISPOSITION)
866            .expect("To have CONTENT_DISPOSITION");
867        let content_disposition =
868            content_disposition.to_str().expect("Convert CONTENT_DISPOSITION to str");
869        assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\"");
870    }
871
872    #[ntex::test]
873    async fn test_named_file_ranges_status_code() {
874        let srv = test::init_service(
875            App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
876        )
877        .await;
878
879        // Valid range header
880        let request = TestRequest::get()
881            .uri("/t%65st/Cargo.toml")
882            .header(http::header::RANGE, "bytes=10-20")
883            .to_request();
884        let response = test::call_service(&srv, request).await;
885        assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
886
887        // Invalid range header
888        let request = TestRequest::get()
889            .uri("/t%65st/Cargo.toml")
890            .header(http::header::RANGE, "bytes=1-0")
891            .to_request();
892        let response = test::call_service(&srv, request).await;
893
894        assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
895    }
896
897    #[ntex::test]
898    async fn test_named_file_content_range_headers() {
899        let srv = test::init_service(
900            App::new().service(Files::new("/test", ".").index_file("tests/test.binary")),
901        )
902        .await;
903
904        // Valid range header
905        let request = TestRequest::get()
906            .uri("/t%65st/tests/test.binary")
907            .header(http::header::RANGE, "bytes=10-20")
908            .to_request();
909
910        let response = test::call_service(&srv, request).await;
911        let contentrange =
912            response.headers().get(http::header::CONTENT_RANGE).unwrap().to_str().unwrap();
913
914        assert_eq!(contentrange, "bytes 10-20/100");
915
916        // Invalid range header
917        let request = TestRequest::get()
918            .uri("/t%65st/tests/test.binary")
919            .header(http::header::RANGE, "bytes=10-5")
920            .to_request();
921        let response = test::call_service(&srv, request).await;
922
923        let contentrange =
924            response.headers().get(http::header::CONTENT_RANGE).unwrap().to_str().unwrap();
925
926        assert_eq!(contentrange, "bytes */100");
927    }
928
929    #[ntex::test]
930    async fn test_named_file_content_length_headers() {
931        let srv = test::init_service(
932            App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
933        )
934        .await;
935
936        // Valid range header
937        let request = TestRequest::get()
938            .uri("/t%65st/tests/test.binary")
939            .header(http::header::RANGE, "bytes=10-20")
940            .to_request();
941        let _response = test::call_service(&srv, request).await;
942
943        // let contentlength = _response
944        //     .headers()
945        //     .get(header::CONTENT_LENGTH)
946        //     .unwrap()
947        //     .to_str()
948        //     .unwrap();
949        // assert_eq!(contentlength, "11");
950
951        // Invalid range header
952        let request = TestRequest::get()
953            .uri("/t%65st/tests/test.binary")
954            .header(http::header::RANGE, "bytes=10-8")
955            .to_request();
956        let response = test::call_service(&srv, request).await;
957        assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
958
959        // Without range header
960        let request = TestRequest::get()
961            .uri("/t%65st/tests/test.binary")
962            // .no_default_headers()
963            .to_request();
964        let _response = test::call_service(&srv, request).await;
965
966        // let contentlength = response
967        //     .headers()
968        //     .get(header::CONTENT_LENGTH)
969        //     .unwrap()
970        //     .to_str()
971        //     .unwrap();
972        // assert_eq!(contentlength, "100");
973
974        // chunked
975        let request = TestRequest::get().uri("/t%65st/tests/test.binary").to_request();
976        let response = test::call_service(&srv, request).await;
977
978        // with enabled compression
979        // {
980        //     let te = response
981        //         .headers()
982        //         .get(header::TRANSFER_ENCODING)
983        //         .unwrap()
984        //         .to_str()
985        //         .unwrap();
986        //     assert_eq!(te, "chunked");
987        // }
988
989        let bytes = test::read_body(response).await;
990        let data = Bytes::from(fs::read("tests/test.binary").unwrap());
991        assert_eq!(bytes, data);
992    }
993
994    #[ntex::test]
995    async fn test_head_content_length_headers() {
996        let srv = test::init_service(
997            App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
998        )
999        .await;
1000
1001        // Valid range header
1002        let request = TestRequest::default()
1003            .method(Method::HEAD)
1004            .uri("/t%65st/tests/test.binary")
1005            .to_request();
1006        let _response = test::call_service(&srv, request).await;
1007
1008        // TODO: fix check
1009        // let contentlength = response
1010        //     .headers()
1011        //     .get(header::CONTENT_LENGTH)
1012        //     .unwrap()
1013        //     .to_str()
1014        //     .unwrap();
1015        // assert_eq!(contentlength, "100");
1016    }
1017
1018    #[ntex::test]
1019    async fn test_static_files_with_spaces() {
1020        let srv = test::init_service(
1021            App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
1022        )
1023        .await;
1024        let request = TestRequest::get().uri("/tests/test%20space.binary").to_request();
1025        let response = test::call_service(&srv, request).await;
1026        assert_eq!(response.status(), StatusCode::OK);
1027
1028        let bytes = test::read_body(response).await;
1029        let data = Bytes::from(fs::read("tests/test space.binary").unwrap());
1030        assert_eq!(bytes, data);
1031    }
1032
1033    #[ntex::test]
1034    async fn test_files_not_allowed() {
1035        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1036
1037        let req = TestRequest::default().uri("/Cargo.toml").method(Method::POST).to_request();
1038
1039        let resp = test::call_service(&srv, req).await;
1040        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
1041
1042        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1043        let req = TestRequest::default().method(Method::PUT).uri("/Cargo.toml").to_request();
1044        let resp = test::call_service(&srv, req).await;
1045        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
1046    }
1047
1048    #[ntex::test]
1049    async fn test_files_guards() {
1050        let srv = test::init_service(
1051            App::new().service(Files::new("/", ".").use_guards(guard::Post())),
1052        )
1053        .await;
1054
1055        let req = TestRequest::default().uri("/Cargo.toml").method(Method::POST).to_request();
1056
1057        let resp = test::call_service(&srv, req).await;
1058        assert_eq!(resp.status(), StatusCode::OK);
1059    }
1060
1061    #[ntex::test]
1062    async fn test_named_file_content_encoding() {
1063        let srv = test::init_service(App::new().wrap(Compress::default()).service(
1064            web::resource("/").to(|| async {
1065                NamedFile::open("Cargo.toml")
1066                    .unwrap()
1067                    .set_content_encoding(http::header::ContentEncoding::Identity)
1068            }),
1069        ))
1070        .await;
1071
1072        let request = TestRequest::get()
1073            .uri("/")
1074            .header(http::header::ACCEPT_ENCODING, "gzip")
1075            .to_request();
1076        let res = test::call_service(&srv, request).await;
1077        assert_eq!(res.status(), StatusCode::OK);
1078        assert!(!res.headers().contains_key(http::header::CONTENT_ENCODING));
1079    }
1080
1081    #[ntex::test]
1082    async fn test_named_file_content_encoding_gzip() {
1083        let srv = test::init_service(App::new().wrap(Compress::default()).service(
1084            web::resource("/").to(|| async {
1085                NamedFile::open("Cargo.toml")
1086                    .unwrap()
1087                    .set_content_encoding(http::header::ContentEncoding::Gzip)
1088            }),
1089        ))
1090        .await;
1091
1092        let request = TestRequest::get()
1093            .uri("/")
1094            .header(http::header::ACCEPT_ENCODING, "gzip")
1095            .to_request();
1096        let res = test::call_service(&srv, request).await;
1097        assert_eq!(res.status(), StatusCode::OK);
1098        assert_eq!(
1099            res.headers().get(http::header::CONTENT_ENCODING).unwrap().to_str().unwrap(),
1100            "gzip"
1101        );
1102    }
1103
1104    #[ntex::test]
1105    async fn test_named_file_allowed_method() {
1106        let req = TestRequest::default().method(Method::GET).to_http_request();
1107        let file = NamedFile::open("Cargo.toml").unwrap();
1108        let resp = test::respond_to(file, &req).await;
1109        assert_eq!(resp.status(), StatusCode::OK);
1110    }
1111
1112    #[ntex::test]
1113    async fn test_static_files() {
1114        let srv =
1115            test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
1116                .await;
1117        let req = TestRequest::with_uri("/missing").to_request();
1118
1119        let resp = test::call_service(&srv, req).await;
1120        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1121
1122        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
1123
1124        let req = TestRequest::default().to_request();
1125        let resp = test::call_service(&srv, req).await;
1126        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1127
1128        let srv =
1129            test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
1130                .await;
1131        let req = TestRequest::with_uri("/tests").to_request();
1132        let resp = test::call_service(&srv, req).await;
1133        assert_eq!(
1134            resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
1135            "text/html; charset=utf-8"
1136        );
1137
1138        let bytes = test::read_body(resp).await;
1139        assert!(format!("{:?}", bytes).contains("/tests/test.png"));
1140    }
1141
1142    #[ntex::test]
1143    async fn test_redirect_to_slash_directory() {
1144        // should not redirect if no index
1145        let srv = test::init_service(
1146            App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
1147        )
1148        .await;
1149        let req = TestRequest::with_uri("/tests").to_request();
1150        let resp = test::call_service(&srv, req).await;
1151        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1152
1153        // should redirect if index present
1154        let srv = test::init_service(App::new().service(
1155            Files::new("/", ".").index_file("test.png").redirect_to_slash_directory(),
1156        ))
1157        .await;
1158        let req = TestRequest::with_uri("/tests").to_request();
1159        let resp = test::call_service(&srv, req).await;
1160        assert_eq!(resp.status(), StatusCode::FOUND);
1161
1162        // should not redirect if the path is wrong
1163        let req = TestRequest::with_uri("/not_existing").to_request();
1164        let resp = test::call_service(&srv, req).await;
1165        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1166    }
1167
1168    #[ntex::test]
1169    async fn test_static_files_bad_directory() {
1170        let _st: Files<DefaultError> = Files::new("/", "missing");
1171        let _st: Files<DefaultError> = Files::new("/", "Cargo.toml");
1172    }
1173
1174    #[ntex::test]
1175    async fn test_default_handler_file_missing() {
1176        let st = Files::new("/", ".")
1177            .default_handler(|req: WebRequest<DefaultError>| async move {
1178                Ok(req.into_response(HttpResponse::Ok().body("default content")))
1179            })
1180            .pipeline(SharedCfg::default())
1181            .await
1182            .unwrap();
1183        let req = TestRequest::with_uri("/missing").to_srv_request();
1184
1185        let resp = test::call_service(&st, req).await;
1186        assert_eq!(resp.status(), StatusCode::OK);
1187        let bytes = test::read_body(resp).await;
1188        assert_eq!(bytes, Bytes::from_static(b"default content"));
1189    }
1190
1191    //     #[ntex::test]
1192    //     async fn test_serve_index() {
1193    //         let st = Files::new(".").index_file("test.binary");
1194    //         let req = TestRequest::default().uri("/tests").finish();
1195
1196    //         let resp = st.handle(&req).respond_to(&req).unwrap();
1197    //         let resp = resp.as_msg();
1198    //         assert_eq!(resp.status(), StatusCode::OK);
1199    //         assert_eq!(
1200    //             resp.headers()
1201    //                 .get(header::CONTENT_TYPE)
1202    //                 .expect("content type"),
1203    //             "application/octet-stream"
1204    //         );
1205    //         assert_eq!(
1206    //             resp.headers()
1207    //                 .get(header::CONTENT_DISPOSITION)
1208    //                 .expect("content disposition"),
1209    //             "attachment; filename=\"test.binary\""
1210    //         );
1211
1212    //         let req = TestRequest::default().uri("/tests/").finish();
1213    //         let resp = st.handle(&req).respond_to(&req).unwrap();
1214    //         let resp = resp.as_msg();
1215    //         assert_eq!(resp.status(), StatusCode::OK);
1216    //         assert_eq!(
1217    //             resp.headers().get(header::CONTENT_TYPE).unwrap(),
1218    //             "application/octet-stream"
1219    //         );
1220    //         assert_eq!(
1221    //             resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
1222    //             "attachment; filename=\"test.binary\""
1223    //         );
1224
1225    //         // nonexistent index file
1226    //         let req = TestRequest::default().uri("/tests/unknown").finish();
1227    //         let resp = st.handle(&req).respond_to(&req).unwrap();
1228    //         let resp = resp.as_msg();
1229    //         assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1230
1231    //         let req = TestRequest::default().uri("/tests/unknown/").finish();
1232    //         let resp = st.handle(&req).respond_to(&req).unwrap();
1233    //         let resp = resp.as_msg();
1234    //         assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1235    //     }
1236
1237    //     #[ntex::test]
1238    //     async fn test_serve_index_nested() {
1239    //         let st = Files::new(".").index_file("mod.rs");
1240    //         let req = TestRequest::default().uri("/src/client").finish();
1241    //         let resp = st.handle(&req).respond_to(&req).unwrap();
1242    //         let resp = resp.as_msg();
1243    //         assert_eq!(resp.status(), StatusCode::OK);
1244    //         assert_eq!(
1245    //             resp.headers().get(header::CONTENT_TYPE).unwrap(),
1246    //             "text/x-rust"
1247    //         );
1248    //         assert_eq!(
1249    //             resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
1250    //             "inline; filename=\"mod.rs\""
1251    //         );
1252    //     }
1253
1254    //     #[ntex::test]
1255    //     fn integration_serve_index() {
1256    //         let srv = test::TestServer::with_factory(|| {
1257    //             App::new().handler(
1258    //                 "test",
1259    //                 Files::new(".").index_file("Cargo.toml"),
1260    //             )
1261    //         });
1262
1263    //         let request = srv.get().uri(srv.url("/test")).finish().unwrap();
1264    //         let response = srv.execute(request.send()).unwrap();
1265    //         assert_eq!(response.status(), StatusCode::OK);
1266    //         let bytes = srv.execute(response.body()).unwrap();
1267    //         let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1268    //         assert_eq!(bytes, data);
1269
1270    //         let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
1271    //         let response = srv.execute(request.send()).unwrap();
1272    //         assert_eq!(response.status(), StatusCode::OK);
1273    //         let bytes = srv.execute(response.body()).unwrap();
1274    //         let data = Bytes::from(fs::read("Cargo.toml").unwrap());
1275    //         assert_eq!(bytes, data);
1276
1277    //         // nonexistent index file
1278    //         let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap();
1279    //         let response = srv.execute(request.send()).unwrap();
1280    //         assert_eq!(response.status(), StatusCode::NOT_FOUND);
1281
1282    //         let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap();
1283    //         let response = srv.execute(request.send()).unwrap();
1284    //         assert_eq!(response.status(), StatusCode::NOT_FOUND);
1285    //     }
1286
1287    //     #[ntex::test]
1288    //     fn integration_percent_encoded() {
1289    //         let srv = test::TestServer::with_factory(|| {
1290    //             App::new().handler(
1291    //                 "test",
1292    //                 Files::new(".").index_file("Cargo.toml"),
1293    //             )
1294    //         });
1295
1296    //         let request = srv
1297    //             .get()
1298    //             .uri(srv.url("/test/%43argo.toml"))
1299    //             .finish()
1300    //             .unwrap();
1301    //         let response = srv.execute(request.send()).unwrap();
1302    //         assert_eq!(response.status(), StatusCode::OK);
1303    //     }
1304
1305    #[ntex::test]
1306    async fn test_path_buf() {
1307        assert_eq!(
1308            PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0),
1309            Err(UriSegmentError::BadStart('.'))
1310        );
1311        assert_eq!(
1312            PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0),
1313            Err(UriSegmentError::BadStart('*'))
1314        );
1315        assert_eq!(
1316            PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0),
1317            Err(UriSegmentError::BadEnd(':'))
1318        );
1319        assert_eq!(
1320            PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0),
1321            Err(UriSegmentError::BadEnd('<'))
1322        );
1323        assert_eq!(
1324            PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0),
1325            Err(UriSegmentError::BadEnd('>'))
1326        );
1327        assert_eq!(
1328            PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0,
1329            PathBuf::from_iter(vec!["seg1", "seg2"])
1330        );
1331        assert_eq!(
1332            PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0,
1333            PathBuf::from_iter(vec!["seg2"])
1334        );
1335    }
1336}