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