Skip to main content

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