1use std::{
2 borrow::Cow,
3 cell::RefCell,
4 ffi::{OsStr, OsString},
5 fmt, io,
6 path::{Path, PathBuf},
7 rc::Rc,
8};
9
10use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
11use actix_web::{
12 dev::{
13 AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse,
14 },
15 error::Error,
16 guard::Guard,
17 http::header::DispositionType,
18 HttpRequest,
19};
20use futures_core::future::LocalBoxFuture;
21
22use crate::{
23 directory_listing, named,
24 service::{FilesService, FilesServiceInner},
25 Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
26};
27
28pub struct Files {
41 mount_path: String,
42 directories: Vec<PathBuf>,
43 index: Option<String>,
44 show_index: bool,
45 redirect_to_slash: bool,
46 with_permanent_redirect: bool,
47 default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
48 renderer: Rc<DirectoryRenderer>,
49 mime_override: Option<Rc<MimeOverride>>,
50 path_filter: Option<Rc<PathFilter>>,
51 file_flags: named::Flags,
52 use_guards: Option<Rc<dyn Guard>>,
53 guards: Vec<Rc<dyn Guard>>,
54 hidden_files: bool,
55 try_compressed: bool,
56 read_mode_threshold: u64,
57}
58
59impl fmt::Debug for Files {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 f.write_str("Files")
62 }
63}
64
65impl Clone for Files {
66 fn clone(&self) -> Self {
67 Self {
68 directories: self.directories.clone(),
69 index: self.index.clone(),
70 show_index: self.show_index,
71 redirect_to_slash: self.redirect_to_slash,
72 with_permanent_redirect: self.with_permanent_redirect,
73 default: self.default.clone(),
74 renderer: self.renderer.clone(),
75 file_flags: self.file_flags,
76 mount_path: self.mount_path.clone(),
77 mime_override: self.mime_override.clone(),
78 path_filter: self.path_filter.clone(),
79 use_guards: self.use_guards.clone(),
80 guards: self.guards.clone(),
81 hidden_files: self.hidden_files,
82 try_compressed: self.try_compressed,
83 read_mode_threshold: self.read_mode_threshold,
84 }
85 }
86}
87
88#[derive(Debug)]
93pub struct FilesDirs(Vec<PathBuf>);
94
95impl FilesDirs {
96 fn canonicalize(self) -> Vec<PathBuf> {
97 self.0
98 .into_iter()
99 .map(|orig_dir| match orig_dir.canonicalize() {
100 Ok(canon_dir) => canon_dir,
101 Err(_) => {
102 log::error!("Specified path is not a directory: {:?}", orig_dir);
103 orig_dir
105 }
106 })
107 .collect()
108 }
109}
110
111impl From<&Path> for FilesDirs {
112 fn from(dir: &Path) -> Self {
113 Self(vec![dir.into()])
114 }
115}
116
117impl From<&PathBuf> for FilesDirs {
118 fn from(dir: &PathBuf) -> Self {
119 Self(vec![dir.into()])
120 }
121}
122
123impl From<PathBuf> for FilesDirs {
124 fn from(dir: PathBuf) -> Self {
125 Self(vec![dir])
126 }
127}
128
129impl From<&str> for FilesDirs {
130 fn from(dir: &str) -> Self {
131 Self(vec![dir.into()])
132 }
133}
134
135impl From<&String> for FilesDirs {
136 fn from(dir: &String) -> Self {
137 Self(vec![dir.into()])
138 }
139}
140
141impl From<String> for FilesDirs {
142 fn from(dir: String) -> Self {
143 Self(vec![dir.into()])
144 }
145}
146
147impl From<&OsStr> for FilesDirs {
148 fn from(dir: &OsStr) -> Self {
149 Self(vec![dir.into()])
150 }
151}
152
153impl From<OsString> for FilesDirs {
154 fn from(dir: OsString) -> Self {
155 Self(vec![dir.into()])
156 }
157}
158
159impl From<&OsString> for FilesDirs {
160 fn from(dir: &OsString) -> Self {
161 Self(vec![dir.into()])
162 }
163}
164
165impl From<Box<Path>> for FilesDirs {
166 fn from(dir: Box<Path>) -> Self {
167 Self(vec![dir.into()])
168 }
169}
170
171impl From<Cow<'_, Path>> for FilesDirs {
172 fn from(dir: Cow<'_, Path>) -> Self {
173 Self(vec![dir.into()])
174 }
175}
176
177impl<P, const N: usize> From<[P; N]> for FilesDirs
178where
179 P: Into<PathBuf>,
180{
181 fn from(dirs: [P; N]) -> Self {
182 Self(dirs.into_iter().map(Into::into).collect())
183 }
184}
185
186impl<P, const N: usize> From<&[P; N]> for FilesDirs
187where
188 P: Clone + Into<PathBuf>,
189{
190 fn from(dirs: &[P; N]) -> Self {
191 Self(dirs.iter().cloned().map(Into::into).collect())
192 }
193}
194
195impl<P> From<&[P]> for FilesDirs
196where
197 P: Clone + Into<PathBuf>,
198{
199 fn from(dirs: &[P]) -> Self {
200 Self(dirs.iter().cloned().map(Into::into).collect())
201 }
202}
203
204impl<P> From<Vec<P>> for FilesDirs
205where
206 P: Into<PathBuf>,
207{
208 fn from(dirs: Vec<P>) -> Self {
209 Self(dirs.into_iter().map(Into::into).collect())
210 }
211}
212
213impl Files {
214 pub fn new<T: Into<FilesDirs>>(mount_path: &str, serve_from: T) -> Files {
245 Files {
246 mount_path: mount_path.trim_end_matches('/').to_owned(),
247 directories: serve_from.into().canonicalize(),
248 index: None,
249 show_index: false,
250 redirect_to_slash: false,
251 with_permanent_redirect: false,
252 default: Rc::new(RefCell::new(None)),
253 renderer: Rc::new(directory_listing),
254 mime_override: None,
255 path_filter: None,
256 file_flags: named::Flags::default(),
257 use_guards: None,
258 guards: Vec::new(),
259 hidden_files: false,
260 try_compressed: false,
261 read_mode_threshold: 0,
262 }
263 }
264
265 pub fn show_files_listing(mut self) -> Self {
272 self.show_index = true;
273 self
274 }
275
276 pub fn redirect_to_slash_directory(mut self) -> Self {
283 self.redirect_to_slash = true;
284 self
285 }
286
287 pub fn with_permanent_redirect(mut self) -> Self {
291 self.with_permanent_redirect = true;
292 self
293 }
294
295 pub fn files_listing_renderer<F>(mut self, f: F) -> Self
297 where
298 for<'r, 's> F:
299 Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
300 {
301 self.renderer = Rc::new(f);
302 self
303 }
304
305 pub fn mime_override<F>(mut self, f: F) -> Self
307 where
308 F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
309 {
310 self.mime_override = Some(Rc::new(f));
311 self
312 }
313
314 pub fn path_filter<F>(mut self, f: F) -> Self
339 where
340 F: Fn(&Path, &RequestHead) -> bool + 'static,
341 {
342 self.path_filter = Some(Rc::new(f));
343 self
344 }
345
346 pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
358 self.index = Some(index.into());
359 self
360 }
361
362 pub fn read_mode_threshold(mut self, size: u64) -> Self {
373 self.read_mode_threshold = size;
374 self
375 }
376
377 pub fn use_etag(mut self, value: bool) -> Self {
381 self.file_flags.set(named::Flags::ETAG, value);
382 self
383 }
384
385 pub fn use_last_modified(mut self, value: bool) -> Self {
389 self.file_flags.set(named::Flags::LAST_MD, value);
390 self
391 }
392
393 pub fn prefer_utf8(mut self, value: bool) -> Self {
397 self.file_flags.set(named::Flags::PREFER_UTF8, value);
398 self
399 }
400
401 pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
421 self.guards.push(Rc::new(guard));
422 self
423 }
424
425 pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
431 self.use_guards = Some(Rc::new(guard));
432 self
433 }
434
435 #[doc(hidden)]
437 #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
438 pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
439 self.method_guard(guard)
440 }
441
442 pub fn disable_content_disposition(mut self) -> Self {
446 self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
447 self
448 }
449
450 pub fn default_handler<F, U>(mut self, f: F) -> Self
471 where
472 F: IntoServiceFactory<U, ServiceRequest>,
473 U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
474 + 'static,
475 {
476 self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
478 f.into_factory().map_init_err(|_| ()),
479 )))));
480
481 self
482 }
483
484 pub fn use_hidden_files(mut self) -> Self {
486 self.hidden_files = true;
487 self
488 }
489
490 pub fn try_compressed(mut self) -> Self {
495 self.try_compressed = true;
496 self
497 }
498}
499
500impl HttpServiceFactory for Files {
501 fn register(mut self, config: &mut AppService) {
502 let guards = if self.guards.is_empty() {
503 None
504 } else {
505 let guards = std::mem::take(&mut self.guards);
506 Some(
507 guards
508 .into_iter()
509 .map(|guard| -> Box<dyn Guard> { Box::new(guard) })
510 .collect::<Vec<_>>(),
511 )
512 };
513
514 if self.default.borrow().is_none() {
515 *self.default.borrow_mut() = Some(config.default_service());
516 }
517
518 let rdef = if config.is_root() {
519 ResourceDef::root_prefix(&self.mount_path)
520 } else {
521 ResourceDef::prefix(&self.mount_path)
522 };
523
524 config.register_service(rdef, guards, self, None)
525 }
526}
527
528impl ServiceFactory<ServiceRequest> for Files {
529 type Response = ServiceResponse;
530 type Error = Error;
531 type Config = ();
532 type Service = FilesService;
533 type InitError = ();
534 type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
535
536 fn new_service(&self, _: ()) -> Self::Future {
537 let mut inner = FilesServiceInner {
538 directories: self.directories.clone(),
539 index: self.index.clone(),
540 show_index: self.show_index,
541 redirect_to_slash: self.redirect_to_slash,
542 default: None,
543 renderer: self.renderer.clone(),
544 mime_override: self.mime_override.clone(),
545 path_filter: self.path_filter.clone(),
546 file_flags: self.file_flags,
547 guards: self.use_guards.clone(),
548 hidden_files: self.hidden_files,
549 try_compressed: self.try_compressed,
550 size_threshold: self.read_mode_threshold,
551 with_permanent_redirect: self.with_permanent_redirect,
552 };
553
554 if let Some(ref default) = *self.default.borrow() {
555 let fut = default.new_service(());
556 Box::pin(async {
557 match fut.await {
558 Ok(default) => {
559 inner.default = Some(default);
560 Ok(FilesService(Rc::new(inner)))
561 }
562 Err(_) => Err(()),
563 }
564 })
565 } else {
566 Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
567 }
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use actix_web::{
574 http::StatusCode,
575 test::{self, TestRequest},
576 App, HttpResponse,
577 };
578
579 use super::*;
580
581 #[actix_web::test]
582 async fn custom_files_listing_renderer() {
583 let srv = test::init_service(
584 App::new().service(
585 Files::new("/", "./tests")
586 .show_files_listing()
587 .files_listing_renderer(|dir, req| {
588 Ok(ServiceResponse::new(
589 req.clone(),
590 HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
591 ))
592 }),
593 ),
594 )
595 .await;
596
597 let req = TestRequest::with_uri("/").to_request();
598 let res = test::call_service(&srv, req).await;
599
600 assert_eq!(res.status(), StatusCode::OK);
601 let body = test::read_body(res).await;
602 let body_str = std::str::from_utf8(&body).unwrap();
603 let actual_path = Path::new(&body_str);
604 let expected_path = Path::new("actix-files/tests");
605 assert!(
606 actual_path.ends_with(expected_path),
607 "body {:?} does not end with {:?}",
608 actual_path,
609 expected_path
610 );
611 }
612}