1use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
2
3use actix_web::{
4 body::BoxBody,
5 dev::{self, Service, ServiceRequest, ServiceResponse},
6 error::Error,
7 guard::Guard,
8 http::{header, Method},
9 HttpResponse,
10};
11use futures_core::future::LocalBoxFuture;
12
13use crate::{
14 named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
15 PathBufWrap, PathFilter,
16};
17
18#[derive(Clone)]
20pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
21
22impl Deref for FilesService {
23 type Target = FilesServiceInner;
24
25 fn deref(&self) -> &Self::Target {
26 &self.0
27 }
28}
29
30pub struct FilesServiceInner {
31 pub(crate) directory: PathBuf,
32 pub(crate) index: Option<String>,
33 pub(crate) show_index: bool,
34 pub(crate) redirect_to_slash: bool,
35 pub(crate) default: Option<HttpService>,
36 pub(crate) renderer: Rc<DirectoryRenderer>,
37 pub(crate) mime_override: Option<Rc<MimeOverride>>,
38 pub(crate) path_filter: Option<Rc<PathFilter>>,
39 pub(crate) file_flags: named::Flags,
40 pub(crate) guards: Option<Rc<dyn Guard>>,
41 pub(crate) hidden_files: bool,
42 pub(crate) size_threshold: u64,
43 pub(crate) with_permanent_redirect: bool,
44}
45
46impl fmt::Debug for FilesServiceInner {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str("FilesServiceInner")
49 }
50}
51
52impl FilesService {
53 async fn handle_err(
54 &self,
55 err: io::Error,
56 req: ServiceRequest,
57 ) -> Result<ServiceResponse, Error> {
58 log::debug!("error handling {}: {}", req.path(), err);
59
60 if let Some(ref default) = self.default {
61 default.call(req).await
62 } else {
63 Ok(req.error_response(err))
64 }
65 }
66
67 fn serve_named_file(&self, req: ServiceRequest, mut named_file: NamedFile) -> ServiceResponse {
68 if let Some(ref mime_override) = self.mime_override {
69 let new_disposition = mime_override(&named_file.content_type.type_());
70 named_file.content_disposition.disposition = new_disposition;
71 }
72 named_file.flags = self.file_flags;
73
74 let (req, _) = req.into_parts();
75 let res = named_file
76 .read_mode_threshold(self.size_threshold)
77 .into_response(&req);
78 ServiceResponse::new(req, res)
79 }
80
81 fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
82 let dir = Directory::new(self.directory.clone(), path);
83
84 let (req, _) = req.into_parts();
85
86 (self.renderer)(&dir, &req).unwrap_or_else(|err| ServiceResponse::from_err(err, req))
87 }
88}
89
90impl fmt::Debug for FilesService {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.write_str("FilesService")
93 }
94}
95
96impl Service<ServiceRequest> for FilesService {
97 type Response = ServiceResponse<BoxBody>;
98 type Error = Error;
99 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
100
101 dev::always_ready!();
102
103 fn call(&self, req: ServiceRequest) -> Self::Future {
104 let is_method_valid = if let Some(guard) = &self.guards {
105 (**guard).check(&req.guard_ctx())
107 } else {
108 matches!(*req.method(), Method::HEAD | Method::GET)
110 };
111
112 let this = self.clone();
113
114 Box::pin(async move {
115 if !is_method_valid {
116 return Ok(req.into_response(
117 HttpResponse::MethodNotAllowed()
118 .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
119 .body("Request did not meet this resource's requirements."),
120 ));
121 }
122
123 let path_on_disk =
124 match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) {
125 Ok(item) => item,
126 Err(err) => return Ok(req.error_response(err)),
127 };
128
129 if let Some(filter) = &this.path_filter {
130 if !filter(path_on_disk.as_ref(), req.head()) {
131 if let Some(ref default) = this.default {
132 return default.call(req).await;
133 } else {
134 return Ok(req.into_response(HttpResponse::NotFound().finish()));
135 }
136 }
137 }
138
139 let path = this.directory.join(&path_on_disk);
141 if let Err(err) = path.canonicalize() {
142 return this.handle_err(err, req).await;
143 }
144
145 if path.is_dir() {
146 if this.redirect_to_slash
147 && !req.path().ends_with('/')
148 && (this.index.is_some() || this.show_index)
149 {
150 let redirect_to = format!("{}/", req.path());
151
152 let response = if this.with_permanent_redirect {
153 HttpResponse::PermanentRedirect()
154 } else {
155 HttpResponse::TemporaryRedirect()
156 }
157 .insert_header((header::LOCATION, redirect_to))
158 .finish();
159
160 return Ok(req.into_response(response));
161 }
162
163 match this.index {
164 Some(ref index) => {
165 let named_path = path.join(index);
166 match NamedFile::open_async(named_path).await {
167 Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
168 Err(_) if this.show_index => Ok(this.show_index(req, path)),
169 Err(err) => this.handle_err(err, req).await,
170 }
171 }
172 None if this.show_index => Ok(this.show_index(req, path)),
173 None => Ok(ServiceResponse::from_err(
174 FilesError::IsDirectory,
175 req.into_parts().0,
176 )),
177 }
178 } else {
179 match NamedFile::open_async(&path).await {
180 Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
181 Err(err) => this.handle_err(err, req).await,
182 }
183 }
184 })
185 }
186}