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