actix_files/
lib.rs

1//! Static file serving for Actix Web.
2//!
3//! Provides a non-blocking service for serving static files from disk.
4//!
5//! # Examples
6//! ```
7//! use actix_web::App;
8//! use actix_files::Files;
9//!
10//! let app = App::new()
11//!     .service(Files::new("/static", ".").prefer_utf8(true));
12//! ```
13
14#![deny(rust_2018_idioms, nonstandard_style)]
15#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
16#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
17#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
18#![cfg_attr(docsrs, feature(doc_auto_cfg))]
19
20use std::path::Path;
21
22use actix_service::boxed::{BoxService, BoxServiceFactory};
23use actix_web::{
24    dev::{RequestHead, ServiceRequest, ServiceResponse},
25    error::Error,
26    http::header::DispositionType,
27};
28use mime_guess::from_ext;
29
30mod chunked;
31mod directory;
32mod encoding;
33mod error;
34mod files;
35mod named;
36mod path_buf;
37mod range;
38mod service;
39
40pub use self::{
41    chunked::ChunkedReadFile, directory::Directory, files::Files, named::NamedFile,
42    range::HttpRange, service::FilesService,
43};
44use self::{
45    directory::{directory_listing, DirectoryRenderer},
46    error::FilesError,
47    path_buf::PathBufWrap,
48};
49
50type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
51type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
52
53/// Return the MIME type associated with a filename extension (case-insensitive).
54/// If `ext` is empty or no associated type for the extension was found, returns
55/// the type `application/octet-stream`.
56#[inline]
57pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
58    from_ext(ext).first_or_octet_stream()
59}
60
61type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
62
63type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
64
65#[cfg(test)]
66mod tests {
67    use std::{
68        fmt::Write as _,
69        fs::{self},
70        ops::Add,
71        time::{Duration, SystemTime},
72    };
73
74    use actix_web::{
75        dev::ServiceFactory,
76        guard,
77        http::{
78            header::{self, ContentDisposition, DispositionParam},
79            Method, StatusCode,
80        },
81        middleware::Compress,
82        test::{self, TestRequest},
83        web::{self, Bytes},
84        App, HttpResponse, Responder,
85    };
86
87    use super::*;
88    use crate::named::File;
89
90    #[actix_web::test]
91    async fn test_file_extension_to_mime() {
92        let m = file_extension_to_mime("");
93        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
94
95        let m = file_extension_to_mime("jpg");
96        assert_eq!(m, mime::IMAGE_JPEG);
97
98        let m = file_extension_to_mime("invalid extension!!");
99        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
100
101        let m = file_extension_to_mime("");
102        assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
103    }
104
105    #[actix_rt::test]
106    async fn test_if_modified_since_without_if_none_match() {
107        let file = NamedFile::open_async("Cargo.toml").await.unwrap();
108        let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
109
110        let req = TestRequest::default()
111            .insert_header((header::IF_MODIFIED_SINCE, since))
112            .to_http_request();
113        let resp = file.respond_to(&req);
114        assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
115    }
116
117    #[actix_rt::test]
118    async fn test_if_modified_since_without_if_none_match_same() {
119        let file = NamedFile::open_async("Cargo.toml").await.unwrap();
120        let since = file.last_modified().unwrap();
121
122        let req = TestRequest::default()
123            .insert_header((header::IF_MODIFIED_SINCE, since))
124            .to_http_request();
125        let resp = file.respond_to(&req);
126        assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
127    }
128
129    #[actix_rt::test]
130    async fn test_if_modified_since_with_if_none_match() {
131        let file = NamedFile::open_async("Cargo.toml").await.unwrap();
132        let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
133
134        let req = TestRequest::default()
135            .insert_header((header::IF_NONE_MATCH, "miss_etag"))
136            .insert_header((header::IF_MODIFIED_SINCE, since))
137            .to_http_request();
138        let resp = file.respond_to(&req);
139        assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
140    }
141
142    #[actix_rt::test]
143    async fn test_if_unmodified_since() {
144        let file = NamedFile::open_async("Cargo.toml").await.unwrap();
145        let since = file.last_modified().unwrap();
146
147        let req = TestRequest::default()
148            .insert_header((header::IF_UNMODIFIED_SINCE, since))
149            .to_http_request();
150        let resp = file.respond_to(&req);
151        assert_eq!(resp.status(), StatusCode::OK);
152    }
153
154    #[actix_rt::test]
155    async fn test_if_unmodified_since_failed() {
156        let file = NamedFile::open_async("Cargo.toml").await.unwrap();
157        let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
158
159        let req = TestRequest::default()
160            .insert_header((header::IF_UNMODIFIED_SINCE, since))
161            .to_http_request();
162        let resp = file.respond_to(&req);
163        assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
164    }
165
166    #[actix_rt::test]
167    async fn test_named_file_text() {
168        assert!(NamedFile::open_async("test--").await.is_err());
169        let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
170        {
171            file.file();
172            let _f: &File = &file;
173        }
174        {
175            let _f: &mut File = &mut file;
176        }
177
178        let req = TestRequest::default().to_http_request();
179        let resp = file.respond_to(&req);
180        assert_eq!(
181            resp.headers().get(header::CONTENT_TYPE).unwrap(),
182            "text/x-toml"
183        );
184        assert_eq!(
185            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
186            "inline; filename=\"Cargo.toml\""
187        );
188    }
189
190    #[actix_rt::test]
191    async fn test_named_file_content_disposition() {
192        assert!(NamedFile::open_async("test--").await.is_err());
193        let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
194        {
195            file.file();
196            let _f: &File = &file;
197        }
198        {
199            let _f: &mut File = &mut file;
200        }
201
202        let req = TestRequest::default().to_http_request();
203        let resp = file.respond_to(&req);
204        assert_eq!(
205            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
206            "inline; filename=\"Cargo.toml\""
207        );
208
209        let file = NamedFile::open_async("Cargo.toml")
210            .await
211            .unwrap()
212            .disable_content_disposition();
213        let req = TestRequest::default().to_http_request();
214        let resp = file.respond_to(&req);
215        assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
216    }
217
218    #[actix_rt::test]
219    async fn test_named_file_non_ascii_file_name() {
220        let file = {
221            #[cfg(feature = "experimental-io-uring")]
222            {
223                crate::named::File::open("Cargo.toml").await.unwrap()
224            }
225
226            #[cfg(not(feature = "experimental-io-uring"))]
227            {
228                crate::named::File::open("Cargo.toml").unwrap()
229            }
230        };
231
232        let mut file = NamedFile::from_file(file, "貨物.toml").unwrap();
233        {
234            file.file();
235            let _f: &File = &file;
236        }
237        {
238            let _f: &mut File = &mut file;
239        }
240
241        let req = TestRequest::default().to_http_request();
242        let resp = file.respond_to(&req);
243        assert_eq!(
244            resp.headers().get(header::CONTENT_TYPE).unwrap(),
245            "text/x-toml"
246        );
247        assert_eq!(
248            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
249            "inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml"
250        );
251    }
252
253    #[actix_rt::test]
254    async fn test_named_file_set_content_type() {
255        let mut file = NamedFile::open_async("Cargo.toml")
256            .await
257            .unwrap()
258            .set_content_type(mime::TEXT_XML);
259        {
260            file.file();
261            let _f: &File = &file;
262        }
263        {
264            let _f: &mut File = &mut file;
265        }
266
267        let req = TestRequest::default().to_http_request();
268        let resp = file.respond_to(&req);
269        assert_eq!(
270            resp.headers().get(header::CONTENT_TYPE).unwrap(),
271            "text/xml"
272        );
273        assert_eq!(
274            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
275            "inline; filename=\"Cargo.toml\""
276        );
277    }
278
279    #[actix_rt::test]
280    async fn test_named_file_image() {
281        let mut file = NamedFile::open_async("tests/test.png").await.unwrap();
282        {
283            file.file();
284            let _f: &File = &file;
285        }
286        {
287            let _f: &mut File = &mut file;
288        }
289
290        let req = TestRequest::default().to_http_request();
291        let resp = file.respond_to(&req);
292        assert_eq!(
293            resp.headers().get(header::CONTENT_TYPE).unwrap(),
294            "image/png"
295        );
296        assert_eq!(
297            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
298            "inline; filename=\"test.png\""
299        );
300    }
301
302    #[actix_rt::test]
303    async fn test_named_file_javascript() {
304        let file = NamedFile::open_async("tests/test.js").await.unwrap();
305
306        let req = TestRequest::default().to_http_request();
307        let resp = file.respond_to(&req);
308        assert_eq!(
309            resp.headers().get(header::CONTENT_TYPE).unwrap(),
310            "application/javascript; charset=utf-8"
311        );
312        assert_eq!(
313            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
314            "inline; filename=\"test.js\""
315        );
316    }
317
318    #[actix_rt::test]
319    async fn test_named_file_image_attachment() {
320        let cd = ContentDisposition {
321            disposition: DispositionType::Attachment,
322            parameters: vec![DispositionParam::Filename(String::from("test.png"))],
323        };
324        let mut file = NamedFile::open_async("tests/test.png")
325            .await
326            .unwrap()
327            .set_content_disposition(cd);
328        {
329            file.file();
330            let _f: &File = &file;
331        }
332        {
333            let _f: &mut File = &mut file;
334        }
335
336        let req = TestRequest::default().to_http_request();
337        let resp = file.respond_to(&req);
338        assert_eq!(
339            resp.headers().get(header::CONTENT_TYPE).unwrap(),
340            "image/png"
341        );
342        assert_eq!(
343            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
344            "attachment; filename=\"test.png\""
345        );
346    }
347
348    #[actix_rt::test]
349    async fn test_named_file_binary() {
350        let mut file = NamedFile::open_async("tests/test.binary").await.unwrap();
351        {
352            file.file();
353            let _f: &File = &file;
354        }
355        {
356            let _f: &mut File = &mut file;
357        }
358
359        let req = TestRequest::default().to_http_request();
360        let resp = file.respond_to(&req);
361        assert_eq!(
362            resp.headers().get(header::CONTENT_TYPE).unwrap(),
363            "application/octet-stream"
364        );
365        assert_eq!(
366            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
367            "attachment; filename=\"test.binary\""
368        );
369    }
370
371    #[allow(deprecated)]
372    #[actix_rt::test]
373    async fn status_code_customize_same_output() {
374        let file1 = NamedFile::open_async("Cargo.toml")
375            .await
376            .unwrap()
377            .set_status_code(StatusCode::NOT_FOUND);
378
379        let file2 = NamedFile::open_async("Cargo.toml")
380            .await
381            .unwrap()
382            .customize()
383            .with_status(StatusCode::NOT_FOUND);
384
385        let req = TestRequest::default().to_http_request();
386        let res1 = file1.respond_to(&req);
387        let res2 = file2.respond_to(&req);
388
389        assert_eq!(res1.status(), StatusCode::NOT_FOUND);
390        assert_eq!(res2.status(), StatusCode::NOT_FOUND);
391    }
392
393    #[actix_rt::test]
394    async fn test_named_file_status_code_text() {
395        let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
396
397        {
398            file.file();
399            let _f: &File = &file;
400        }
401
402        {
403            let _f: &mut File = &mut file;
404        }
405
406        let file = file.customize().with_status(StatusCode::NOT_FOUND);
407
408        let req = TestRequest::default().to_http_request();
409        let resp = file.respond_to(&req);
410        assert_eq!(
411            resp.headers().get(header::CONTENT_TYPE).unwrap(),
412            "text/x-toml"
413        );
414        assert_eq!(
415            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
416            "inline; filename=\"Cargo.toml\""
417        );
418        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
419    }
420
421    #[actix_rt::test]
422    async fn test_mime_override() {
423        fn all_attachment(_: &mime::Name<'_>) -> DispositionType {
424            DispositionType::Attachment
425        }
426
427        let srv = test::init_service(
428            App::new().service(
429                Files::new("/", ".")
430                    .mime_override(all_attachment)
431                    .index_file("Cargo.toml"),
432            ),
433        )
434        .await;
435
436        let request = TestRequest::get().uri("/").to_request();
437        let response = test::call_service(&srv, request).await;
438        assert_eq!(response.status(), StatusCode::OK);
439
440        let content_disposition = response
441            .headers()
442            .get(header::CONTENT_DISPOSITION)
443            .expect("To have CONTENT_DISPOSITION");
444        let content_disposition = content_disposition
445            .to_str()
446            .expect("Convert CONTENT_DISPOSITION to str");
447        assert_eq!(content_disposition, "attachment; filename=\"Cargo.toml\"");
448    }
449
450    #[actix_rt::test]
451    async fn test_named_file_ranges_status_code() {
452        let srv = test::init_service(
453            App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
454        )
455        .await;
456
457        // Valid range header
458        let request = TestRequest::get()
459            .uri("/t%65st/Cargo.toml")
460            .insert_header((header::RANGE, "bytes=10-20"))
461            .to_request();
462        let response = test::call_service(&srv, request).await;
463        assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
464
465        // Invalid range header
466        let request = TestRequest::get()
467            .uri("/t%65st/Cargo.toml")
468            .insert_header((header::RANGE, "bytes=1-0"))
469            .to_request();
470        let response = test::call_service(&srv, request).await;
471
472        assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
473    }
474
475    #[actix_rt::test]
476    async fn test_named_file_content_range_headers() {
477        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
478
479        // Valid range header
480        let response = srv
481            .get("/tests/test.binary")
482            .insert_header((header::RANGE, "bytes=10-20"))
483            .send()
484            .await
485            .unwrap();
486        let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
487        assert_eq!(content_range.to_str().unwrap(), "bytes 10-20/100");
488
489        // Invalid range header
490        let response = srv
491            .get("/tests/test.binary")
492            .insert_header((header::RANGE, "bytes=10-5"))
493            .send()
494            .await
495            .unwrap();
496        let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
497        assert_eq!(content_range.to_str().unwrap(), "bytes */100");
498    }
499
500    #[actix_rt::test]
501    async fn test_named_file_content_length_headers() {
502        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
503
504        // Valid range header
505        let response = srv
506            .get("/tests/test.binary")
507            .insert_header((header::RANGE, "bytes=10-20"))
508            .send()
509            .await
510            .unwrap();
511        let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
512        assert_eq!(content_length.to_str().unwrap(), "11");
513
514        // Valid range header, starting from 0
515        let response = srv
516            .get("/tests/test.binary")
517            .insert_header((header::RANGE, "bytes=0-20"))
518            .send()
519            .await
520            .unwrap();
521        let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
522        assert_eq!(content_length.to_str().unwrap(), "21");
523
524        // Without range header
525        let mut response = srv.get("/tests/test.binary").send().await.unwrap();
526        let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
527        assert_eq!(content_length.to_str().unwrap(), "100");
528
529        // Should be no transfer-encoding
530        let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING);
531        assert!(transfer_encoding.is_none());
532
533        // Check file contents
534        let bytes = response.body().await.unwrap();
535        let data = web::Bytes::from(fs::read("tests/test.binary").unwrap());
536        assert_eq!(bytes, data);
537    }
538
539    #[actix_rt::test]
540    async fn test_head_content_length_headers() {
541        let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
542
543        let response = srv.head("/tests/test.binary").send().await.unwrap();
544
545        let content_length = response
546            .headers()
547            .get(header::CONTENT_LENGTH)
548            .unwrap()
549            .to_str()
550            .unwrap();
551
552        assert_eq!(content_length, "100");
553    }
554
555    #[actix_rt::test]
556    async fn test_static_files_with_spaces() {
557        let srv =
558            test::init_service(App::new().service(Files::new("/", ".").index_file("Cargo.toml")))
559                .await;
560        let request = TestRequest::get()
561            .uri("/tests/test%20space.binary")
562            .to_request();
563        let response = test::call_service(&srv, request).await;
564        assert_eq!(response.status(), StatusCode::OK);
565
566        let bytes = test::read_body(response).await;
567        let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap());
568        assert_eq!(bytes, data);
569    }
570
571    #[cfg(not(target_os = "windows"))]
572    #[actix_rt::test]
573    async fn test_static_files_with_special_characters() {
574        // Create the file we want to test against ad-hoc. We can't check it in as otherwise
575        // Windows can't even checkout this repository.
576        let temp_dir = tempfile::tempdir().unwrap();
577        let file_with_newlines = temp_dir.path().join("test\n\x0B\x0C\rnewline.text");
578        fs::write(&file_with_newlines, "Look at my newlines").unwrap();
579
580        let srv = test::init_service(
581            App::new().service(Files::new("/", temp_dir.path()).index_file("Cargo.toml")),
582        )
583        .await;
584        let request = TestRequest::get()
585            .uri("/test%0A%0B%0C%0Dnewline.text")
586            .to_request();
587        let response = test::call_service(&srv, request).await;
588        assert_eq!(response.status(), StatusCode::OK);
589
590        let bytes = test::read_body(response).await;
591        let data = web::Bytes::from(fs::read(file_with_newlines).unwrap());
592        assert_eq!(bytes, data);
593    }
594
595    #[actix_rt::test]
596    async fn test_files_not_allowed() {
597        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
598
599        let req = TestRequest::default()
600            .uri("/Cargo.toml")
601            .method(Method::POST)
602            .to_request();
603
604        let resp = test::call_service(&srv, req).await;
605        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
606
607        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
608        let req = TestRequest::default()
609            .method(Method::PUT)
610            .uri("/Cargo.toml")
611            .to_request();
612        let resp = test::call_service(&srv, req).await;
613        assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
614    }
615
616    #[actix_rt::test]
617    async fn test_files_guards() {
618        let srv = test::init_service(
619            App::new().service(Files::new("/", ".").method_guard(guard::Post())),
620        )
621        .await;
622
623        let req = TestRequest::default()
624            .uri("/Cargo.toml")
625            .method(Method::POST)
626            .to_request();
627
628        let resp = test::call_service(&srv, req).await;
629        assert_eq!(resp.status(), StatusCode::OK);
630    }
631
632    #[actix_rt::test]
633    async fn test_named_file_content_encoding() {
634        let srv = test::init_service(App::new().wrap(Compress::default()).service(
635            web::resource("/").to(|| async {
636                NamedFile::open_async("Cargo.toml")
637                    .await
638                    .unwrap()
639                    .set_content_encoding(header::ContentEncoding::Identity)
640            }),
641        ))
642        .await;
643
644        let request = TestRequest::get()
645            .uri("/")
646            .insert_header((header::ACCEPT_ENCODING, "gzip"))
647            .to_request();
648        let res = test::call_service(&srv, request).await;
649        assert_eq!(res.status(), StatusCode::OK);
650        assert!(res.headers().contains_key(header::CONTENT_ENCODING));
651        assert!(!test::read_body(res).await.is_empty());
652    }
653
654    #[actix_rt::test]
655    async fn test_named_file_content_encoding_gzip() {
656        let srv = test::init_service(App::new().wrap(Compress::default()).service(
657            web::resource("/").to(|| async {
658                NamedFile::open_async("Cargo.toml")
659                    .await
660                    .unwrap()
661                    .set_content_encoding(header::ContentEncoding::Gzip)
662            }),
663        ))
664        .await;
665
666        let request = TestRequest::get()
667            .uri("/")
668            .insert_header((header::ACCEPT_ENCODING, "gzip"))
669            .to_request();
670        let res = test::call_service(&srv, request).await;
671        assert_eq!(res.status(), StatusCode::OK);
672        assert_eq!(
673            res.headers()
674                .get(header::CONTENT_ENCODING)
675                .unwrap()
676                .to_str()
677                .unwrap(),
678            "gzip"
679        );
680    }
681
682    #[actix_rt::test]
683    async fn test_named_file_allowed_method() {
684        let req = TestRequest::default().method(Method::GET).to_http_request();
685        let file = NamedFile::open_async("Cargo.toml").await.unwrap();
686        let resp = file.respond_to(&req);
687        assert_eq!(resp.status(), StatusCode::OK);
688    }
689
690    #[actix_rt::test]
691    async fn test_static_files() {
692        let srv =
693            test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await;
694        let req = TestRequest::with_uri("/missing").to_request();
695
696        let resp = test::call_service(&srv, req).await;
697        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
698
699        let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
700
701        let req = TestRequest::default().to_request();
702        let resp = test::call_service(&srv, req).await;
703        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
704
705        let srv =
706            test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await;
707        let req = TestRequest::with_uri("/tests").to_request();
708        let resp = test::call_service(&srv, req).await;
709        assert_eq!(
710            resp.headers().get(header::CONTENT_TYPE).unwrap(),
711            "text/html; charset=utf-8"
712        );
713
714        let bytes = test::read_body(resp).await;
715        assert!(format!("{:?}", bytes).contains("/tests/test.png"));
716    }
717
718    #[actix_rt::test]
719    async fn test_redirect_to_slash_directory() {
720        // should not redirect if no index and files listing is disabled
721        let srv = test::init_service(
722            App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
723        )
724        .await;
725        let req = TestRequest::with_uri("/tests").to_request();
726        let resp = test::call_service(&srv, req).await;
727        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
728
729        // should redirect if index present
730        let srv = test::init_service(
731            App::new().service(
732                Files::new("/", ".")
733                    .index_file("test.png")
734                    .redirect_to_slash_directory(),
735            ),
736        )
737        .await;
738        let req = TestRequest::with_uri("/tests").to_request();
739        let resp = test::call_service(&srv, req).await;
740        assert_eq!(resp.status(), StatusCode::FOUND);
741
742        // should redirect if files listing is enabled
743        let srv = test::init_service(
744            App::new().service(
745                Files::new("/", ".")
746                    .show_files_listing()
747                    .redirect_to_slash_directory(),
748            ),
749        )
750        .await;
751        let req = TestRequest::with_uri("/tests").to_request();
752        let resp = test::call_service(&srv, req).await;
753        assert_eq!(resp.status(), StatusCode::FOUND);
754
755        // should not redirect if the path is wrong
756        let req = TestRequest::with_uri("/not_existing").to_request();
757        let resp = test::call_service(&srv, req).await;
758        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
759    }
760
761    #[actix_rt::test]
762    async fn test_static_files_bad_directory() {
763        let service = Files::new("/", "./missing").new_service(()).await.unwrap();
764
765        let req = TestRequest::with_uri("/").to_srv_request();
766        let resp = test::call_service(&service, req).await;
767
768        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
769    }
770
771    #[actix_rt::test]
772    async fn test_default_handler_file_missing() {
773        let st = Files::new("/", ".")
774            .default_handler(|req: ServiceRequest| async {
775                Ok(req.into_response(HttpResponse::Ok().body("default content")))
776            })
777            .new_service(())
778            .await
779            .unwrap();
780        let req = TestRequest::with_uri("/missing").to_srv_request();
781        let resp = test::call_service(&st, req).await;
782
783        assert_eq!(resp.status(), StatusCode::OK);
784        let bytes = test::read_body(resp).await;
785        assert_eq!(bytes, web::Bytes::from_static(b"default content"));
786    }
787
788    #[actix_rt::test]
789    async fn test_serve_index_nested() {
790        let service = Files::new(".", ".")
791            .index_file("lib.rs")
792            .new_service(())
793            .await
794            .unwrap();
795
796        let req = TestRequest::default().uri("/src").to_srv_request();
797        let resp = test::call_service(&service, req).await;
798
799        assert_eq!(resp.status(), StatusCode::OK);
800        assert_eq!(
801            resp.headers().get(header::CONTENT_TYPE).unwrap(),
802            "text/x-rust"
803        );
804        assert_eq!(
805            resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
806            "inline; filename=\"lib.rs\""
807        );
808    }
809
810    #[actix_rt::test]
811    async fn integration_serve_index() {
812        let srv = test::init_service(
813            App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
814        )
815        .await;
816
817        let req = TestRequest::get().uri("/test").to_request();
818        let res = test::call_service(&srv, req).await;
819        assert_eq!(res.status(), StatusCode::OK);
820
821        let bytes = test::read_body(res).await;
822
823        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
824        assert_eq!(bytes, data);
825
826        let req = TestRequest::get().uri("/test/").to_request();
827        let res = test::call_service(&srv, req).await;
828        assert_eq!(res.status(), StatusCode::OK);
829
830        let bytes = test::read_body(res).await;
831        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
832        assert_eq!(bytes, data);
833
834        // nonexistent index file
835        let req = TestRequest::get().uri("/test/unknown").to_request();
836        let res = test::call_service(&srv, req).await;
837        assert_eq!(res.status(), StatusCode::NOT_FOUND);
838
839        let req = TestRequest::get().uri("/test/unknown/").to_request();
840        let res = test::call_service(&srv, req).await;
841        assert_eq!(res.status(), StatusCode::NOT_FOUND);
842    }
843
844    #[actix_rt::test]
845    async fn integration_percent_encoded() {
846        let srv = test::init_service(
847            App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
848        )
849        .await;
850
851        let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
852        let res = test::call_service(&srv, req).await;
853        assert_eq!(res.status(), StatusCode::OK);
854
855        // `%2F` == `/`
856        let req = TestRequest::get().uri("/test%2Ftest.binary").to_request();
857        let res = test::call_service(&srv, req).await;
858        assert_eq!(res.status(), StatusCode::NOT_FOUND);
859
860        let req = TestRequest::get().uri("/test/Cargo.toml%00").to_request();
861        let res = test::call_service(&srv, req).await;
862        assert_eq!(res.status(), StatusCode::NOT_FOUND);
863    }
864
865    #[actix_rt::test]
866    async fn test_percent_encoding_2() {
867        let temp_dir = tempfile::tempdir().unwrap();
868        let filename = match cfg!(unix) {
869            true => "ض:?#[]{}<>()@!$&'`|*+,;= %20\n.test",
870            false => "ض#[]{}()@!$&'`+,;= %20.test",
871        };
872        let filename_encoded = filename
873            .as_bytes()
874            .iter()
875            .fold(String::new(), |mut buf, c| {
876                write!(&mut buf, "%{:02X}", c).unwrap();
877                buf
878            });
879        std::fs::File::create(temp_dir.path().join(filename)).unwrap();
880
881        let srv = test::init_service(App::new().service(Files::new("/", temp_dir.path()))).await;
882
883        let req = TestRequest::get()
884            .uri(&format!("/{}", filename_encoded))
885            .to_request();
886        let res = test::call_service(&srv, req).await;
887        assert_eq!(res.status(), StatusCode::OK);
888    }
889
890    #[actix_rt::test]
891    async fn test_serve_named_file() {
892        let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
893        let srv = test::init_service(App::new().service(factory)).await;
894
895        let req = TestRequest::get().uri("/Cargo.toml").to_request();
896        let res = test::call_service(&srv, req).await;
897        assert_eq!(res.status(), StatusCode::OK);
898
899        let bytes = test::read_body(res).await;
900        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
901        assert_eq!(bytes, data);
902
903        let req = TestRequest::get().uri("/test/unknown").to_request();
904        let res = test::call_service(&srv, req).await;
905        assert_eq!(res.status(), StatusCode::NOT_FOUND);
906    }
907
908    #[actix_rt::test]
909    async fn test_serve_named_file_prefix() {
910        let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
911        let srv =
912            test::init_service(App::new().service(web::scope("/test").service(factory))).await;
913
914        let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
915        let res = test::call_service(&srv, req).await;
916        assert_eq!(res.status(), StatusCode::OK);
917
918        let bytes = test::read_body(res).await;
919        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
920        assert_eq!(bytes, data);
921
922        let req = TestRequest::get().uri("/Cargo.toml").to_request();
923        let res = test::call_service(&srv, req).await;
924        assert_eq!(res.status(), StatusCode::NOT_FOUND);
925    }
926
927    #[actix_rt::test]
928    async fn test_named_file_default_service() {
929        let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
930        let srv = test::init_service(App::new().default_service(factory)).await;
931
932        for route in ["/foobar", "/baz", "/"].iter() {
933            let req = TestRequest::get().uri(route).to_request();
934            let res = test::call_service(&srv, req).await;
935            assert_eq!(res.status(), StatusCode::OK);
936
937            let bytes = test::read_body(res).await;
938            let data = Bytes::from(fs::read("Cargo.toml").unwrap());
939            assert_eq!(bytes, data);
940        }
941    }
942
943    #[actix_rt::test]
944    async fn test_default_handler_named_file() {
945        let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
946        let st = Files::new("/", ".")
947            .default_handler(factory)
948            .new_service(())
949            .await
950            .unwrap();
951        let req = TestRequest::with_uri("/missing").to_srv_request();
952        let resp = test::call_service(&st, req).await;
953
954        assert_eq!(resp.status(), StatusCode::OK);
955        let bytes = test::read_body(resp).await;
956        let data = Bytes::from(fs::read("Cargo.toml").unwrap());
957        assert_eq!(bytes, data);
958    }
959
960    #[actix_rt::test]
961    async fn test_symlinks() {
962        let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
963
964        let req = TestRequest::get()
965            .uri("/test/tests/symlink-test.png")
966            .to_request();
967        let res = test::call_service(&srv, req).await;
968        assert_eq!(res.status(), StatusCode::OK);
969        assert_eq!(
970            res.headers().get(header::CONTENT_DISPOSITION).unwrap(),
971            "inline; filename=\"symlink-test.png\""
972        );
973    }
974
975    #[actix_rt::test]
976    async fn test_index_with_show_files_listing() {
977        let service = Files::new(".", ".")
978            .index_file("lib.rs")
979            .show_files_listing()
980            .new_service(())
981            .await
982            .unwrap();
983
984        // Serve the index if exists
985        let req = TestRequest::default().uri("/src").to_srv_request();
986        let resp = test::call_service(&service, req).await;
987        assert_eq!(resp.status(), StatusCode::OK);
988        assert_eq!(
989            resp.headers().get(header::CONTENT_TYPE).unwrap(),
990            "text/x-rust"
991        );
992
993        // Show files listing, otherwise.
994        let req = TestRequest::default().uri("/tests").to_srv_request();
995        let resp = test::call_service(&service, req).await;
996        assert_eq!(
997            resp.headers().get(header::CONTENT_TYPE).unwrap(),
998            "text/html; charset=utf-8"
999        );
1000        let bytes = test::read_body(resp).await;
1001        assert!(format!("{:?}", bytes).contains("/tests/test.png"));
1002    }
1003
1004    #[actix_rt::test]
1005    async fn test_path_filter() {
1006        // prevent searching subdirectories
1007        let st = Files::new("/", ".")
1008            .path_filter(|path, _| path.components().count() == 1)
1009            .new_service(())
1010            .await
1011            .unwrap();
1012
1013        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1014        let resp = test::call_service(&st, req).await;
1015        assert_eq!(resp.status(), StatusCode::OK);
1016
1017        let req = TestRequest::with_uri("/src/lib.rs").to_srv_request();
1018        let resp = test::call_service(&st, req).await;
1019        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1020    }
1021
1022    #[actix_rt::test]
1023    async fn test_default_handler_filter() {
1024        let st = Files::new("/", ".")
1025            .default_handler(|req: ServiceRequest| async {
1026                Ok(req.into_response(HttpResponse::Ok().body("default content")))
1027            })
1028            .path_filter(|path, _| path.extension() == Some("png".as_ref()))
1029            .new_service(())
1030            .await
1031            .unwrap();
1032        let req = TestRequest::with_uri("/Cargo.toml").to_srv_request();
1033        let resp = test::call_service(&st, req).await;
1034
1035        assert_eq!(resp.status(), StatusCode::OK);
1036        let bytes = test::read_body(resp).await;
1037        assert_eq!(bytes, web::Bytes::from_static(b"default content"));
1038    }
1039}