actix_files/
service.rs

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/// Assembled file serving service.
19#[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}
43
44impl fmt::Debug for FilesServiceInner {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str("FilesServiceInner")
47    }
48}
49
50impl FilesService {
51    async fn handle_err(
52        &self,
53        err: io::Error,
54        req: ServiceRequest,
55    ) -> Result<ServiceResponse, Error> {
56        log::debug!("error handling {}: {}", req.path(), err);
57
58        if let Some(ref default) = self.default {
59            default.call(req).await
60        } else {
61            Ok(req.error_response(err))
62        }
63    }
64
65    fn serve_named_file(&self, req: ServiceRequest, mut named_file: NamedFile) -> ServiceResponse {
66        if let Some(ref mime_override) = self.mime_override {
67            let new_disposition = mime_override(&named_file.content_type.type_());
68            named_file.content_disposition.disposition = new_disposition;
69        }
70        named_file.flags = self.file_flags;
71
72        let (req, _) = req.into_parts();
73        let res = named_file.into_response(&req);
74        ServiceResponse::new(req, res)
75    }
76
77    fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
78        let dir = Directory::new(self.directory.clone(), path);
79
80        let (req, _) = req.into_parts();
81
82        (self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
83    }
84}
85
86impl fmt::Debug for FilesService {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.write_str("FilesService")
89    }
90}
91
92impl Service<ServiceRequest> for FilesService {
93    type Response = ServiceResponse<BoxBody>;
94    type Error = Error;
95    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
96
97    dev::always_ready!();
98
99    fn call(&self, req: ServiceRequest) -> Self::Future {
100        let is_method_valid = if let Some(guard) = &self.guards {
101            // execute user defined guards
102            (**guard).check(&req.guard_ctx())
103        } else {
104            // default behavior
105            matches!(*req.method(), Method::HEAD | Method::GET)
106        };
107
108        let this = self.clone();
109
110        Box::pin(async move {
111            if !is_method_valid {
112                return Ok(req.into_response(
113                    HttpResponse::MethodNotAllowed()
114                        .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
115                        .body("Request did not meet this resource's requirements."),
116                ));
117            }
118
119            let path_on_disk =
120                match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) {
121                    Ok(item) => item,
122                    Err(err) => return Ok(req.error_response(err)),
123                };
124
125            if let Some(filter) = &this.path_filter {
126                if !filter(path_on_disk.as_ref(), req.head()) {
127                    if let Some(ref default) = this.default {
128                        return default.call(req).await;
129                    } else {
130                        return Ok(req.into_response(HttpResponse::NotFound().finish()));
131                    }
132                }
133            }
134
135            // full file path
136            let path = this.directory.join(&path_on_disk);
137            if let Err(err) = path.canonicalize() {
138                return this.handle_err(err, req).await;
139            }
140
141            if path.is_dir() {
142                if this.redirect_to_slash
143                    && !req.path().ends_with('/')
144                    && (this.index.is_some() || this.show_index)
145                {
146                    let redirect_to = format!("{}/", req.path());
147
148                    return Ok(req.into_response(
149                        HttpResponse::Found()
150                            .insert_header((header::LOCATION, redirect_to))
151                            .finish(),
152                    ));
153                }
154
155                match this.index {
156                    Some(ref index) => {
157                        let named_path = path.join(index);
158                        match NamedFile::open_async(named_path).await {
159                            Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
160                            Err(_) if this.show_index => Ok(this.show_index(req, path)),
161                            Err(err) => this.handle_err(err, req).await,
162                        }
163                    }
164                    None if this.show_index => Ok(this.show_index(req, path)),
165                    None => Ok(ServiceResponse::from_err(
166                        FilesError::IsDirectory,
167                        req.into_parts().0,
168                    )),
169                }
170            } else {
171                match NamedFile::open_async(&path).await {
172                    Ok(mut named_file) => {
173                        if let Some(ref mime_override) = this.mime_override {
174                            let new_disposition = mime_override(&named_file.content_type.type_());
175                            named_file.content_disposition.disposition = new_disposition;
176                        }
177                        named_file.flags = this.file_flags;
178
179                        let (req, _) = req.into_parts();
180                        let res = named_file.into_response(&req);
181                        Ok(ServiceResponse::new(req, res))
182                    }
183                    Err(err) => this.handle_err(err, req).await,
184                }
185            }
186        })
187    }
188}