clia_ntex_files_mod/
lib.rs

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