actix_files/
files.rs

1use std::{
2    cell::RefCell,
3    fmt, io,
4    path::{Path, PathBuf},
5    rc::Rc,
6};
7
8use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
9use actix_web::{
10    dev::{
11        AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse,
12    },
13    error::Error,
14    guard::Guard,
15    http::header::DispositionType,
16    HttpRequest,
17};
18use futures_core::future::LocalBoxFuture;
19
20use crate::{
21    directory_listing, named,
22    service::{FilesService, FilesServiceInner},
23    Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
24};
25
26/// Static files handling service.
27///
28/// `Files` service must be registered with `App::service()` method.
29///
30/// # Examples
31/// ```
32/// use actix_web::App;
33/// use actix_files::Files;
34///
35/// let app = App::new()
36///     .service(Files::new("/static", "."));
37/// ```
38pub struct Files {
39    mount_path: String,
40    directory: PathBuf,
41    index: Option<String>,
42    show_index: bool,
43    redirect_to_slash: bool,
44    default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
45    renderer: Rc<DirectoryRenderer>,
46    mime_override: Option<Rc<MimeOverride>>,
47    path_filter: Option<Rc<PathFilter>>,
48    file_flags: named::Flags,
49    use_guards: Option<Rc<dyn Guard>>,
50    guards: Vec<Rc<dyn Guard>>,
51    hidden_files: bool,
52}
53
54impl fmt::Debug for Files {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str("Files")
57    }
58}
59
60impl Clone for Files {
61    fn clone(&self) -> Self {
62        Self {
63            directory: self.directory.clone(),
64            index: self.index.clone(),
65            show_index: self.show_index,
66            redirect_to_slash: self.redirect_to_slash,
67            default: self.default.clone(),
68            renderer: self.renderer.clone(),
69            file_flags: self.file_flags,
70            mount_path: self.mount_path.clone(),
71            mime_override: self.mime_override.clone(),
72            path_filter: self.path_filter.clone(),
73            use_guards: self.use_guards.clone(),
74            guards: self.guards.clone(),
75            hidden_files: self.hidden_files,
76        }
77    }
78}
79
80impl Files {
81    /// Create new `Files` instance for a specified base directory.
82    ///
83    /// # Argument Order
84    /// The first argument (`mount_path`) is the root URL at which the static files are served.
85    /// For example, `/assets` will serve files at `example.com/assets/...`.
86    ///
87    /// The second argument (`serve_from`) is the location on disk at which files are loaded.
88    /// This can be a relative path. For example, `./` would serve files from the current
89    /// working directory.
90    ///
91    /// # Implementation Notes
92    /// If the mount path is set as the root path `/`, services registered after this one will
93    /// be inaccessible. Register more specific handlers and services first.
94    ///
95    /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
96    /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
97    /// the number of server [workers](actix_web::HttpServer::workers), by default.
98    pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
99        let orig_dir = serve_from.into();
100        let dir = match orig_dir.canonicalize() {
101            Ok(canon_dir) => canon_dir,
102            Err(_) => {
103                log::error!("Specified path is not a directory: {:?}", orig_dir);
104                PathBuf::new()
105            }
106        };
107
108        Files {
109            mount_path: mount_path.trim_end_matches('/').to_owned(),
110            directory: dir,
111            index: None,
112            show_index: false,
113            redirect_to_slash: false,
114            default: Rc::new(RefCell::new(None)),
115            renderer: Rc::new(directory_listing),
116            mime_override: None,
117            path_filter: None,
118            file_flags: named::Flags::default(),
119            use_guards: None,
120            guards: Vec::new(),
121            hidden_files: false,
122        }
123    }
124
125    /// Show files listing for directories.
126    ///
127    /// By default show files listing is disabled.
128    ///
129    /// When used with [`Files::index_file()`], files listing is shown as a fallback
130    /// when the index file is not found.
131    pub fn show_files_listing(mut self) -> Self {
132        self.show_index = true;
133        self
134    }
135
136    /// Redirects to a slash-ended path when browsing a directory.
137    ///
138    /// By default never redirect.
139    pub fn redirect_to_slash_directory(mut self) -> Self {
140        self.redirect_to_slash = true;
141        self
142    }
143
144    /// Set custom directory renderer.
145    pub fn files_listing_renderer<F>(mut self, f: F) -> Self
146    where
147        for<'r, 's> F:
148            Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
149    {
150        self.renderer = Rc::new(f);
151        self
152    }
153
154    /// Specifies MIME override callback.
155    pub fn mime_override<F>(mut self, f: F) -> Self
156    where
157        F: Fn(&mime::Name<'_>) -> DispositionType + 'static,
158    {
159        self.mime_override = Some(Rc::new(f));
160        self
161    }
162
163    /// Sets path filtering closure.
164    ///
165    /// The path provided to the closure is relative to `serve_from` path.
166    /// You can safely join this path with the `serve_from` path to get the real path.
167    /// However, the real path may not exist since the filter is called before checking path existence.
168    ///
169    /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise,
170    /// `404 Not Found` is returned.
171    ///
172    /// # Examples
173    /// ```
174    /// use std::path::Path;
175    /// use actix_files::Files;
176    ///
177    /// // prevent searching subdirectories and following symlinks
178    /// let files_service = Files::new("/", "./static").path_filter(|path, _| {
179    ///     path.components().count() == 1
180    ///         && Path::new("./static")
181    ///             .join(path)
182    ///             .symlink_metadata()
183    ///             .map(|m| !m.file_type().is_symlink())
184    ///             .unwrap_or(false)
185    /// });
186    /// ```
187    pub fn path_filter<F>(mut self, f: F) -> Self
188    where
189        F: Fn(&Path, &RequestHead) -> bool + 'static,
190    {
191        self.path_filter = Some(Rc::new(f));
192        self
193    }
194
195    /// Set index file
196    ///
197    /// Shows specific index file for directories instead of
198    /// showing files listing.
199    ///
200    /// If the index file is not found, files listing is shown as a fallback if
201    /// [`Files::show_files_listing()`] is set.
202    pub fn index_file<T: Into<String>>(mut self, index: T) -> Self {
203        self.index = Some(index.into());
204        self
205    }
206
207    /// Specifies whether to use ETag or not.
208    ///
209    /// Default is true.
210    pub fn use_etag(mut self, value: bool) -> Self {
211        self.file_flags.set(named::Flags::ETAG, value);
212        self
213    }
214
215    /// Specifies whether to use Last-Modified or not.
216    ///
217    /// Default is true.
218    pub fn use_last_modified(mut self, value: bool) -> Self {
219        self.file_flags.set(named::Flags::LAST_MD, value);
220        self
221    }
222
223    /// Specifies whether text responses should signal a UTF-8 encoding.
224    ///
225    /// Default is false (but will default to true in a future version).
226    pub fn prefer_utf8(mut self, value: bool) -> Self {
227        self.file_flags.set(named::Flags::PREFER_UTF8, value);
228        self
229    }
230
231    /// Adds a routing guard.
232    ///
233    /// Use this to allow multiple chained file services that respond to strictly different
234    /// properties of a request. Due to the way routing works, if a guard check returns true and the
235    /// request starts being handled by the file service, it will not be able to back-out and try
236    /// the next service, you will simply get a 404 (or 405) error response.
237    ///
238    /// To allow `POST` requests to retrieve files, see [`Files::method_guard()`].
239    ///
240    /// # Examples
241    /// ```
242    /// use actix_web::{guard::Header, App};
243    /// use actix_files::Files;
244    ///
245    /// App::new().service(
246    ///     Files::new("/","/my/site/files")
247    ///         .guard(Header("Host", "example.com"))
248    /// );
249    /// ```
250    pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
251        self.guards.push(Rc::new(guard));
252        self
253    }
254
255    /// Specifies guard to check before fetching directory listings or files.
256    ///
257    /// Note that this guard has no effect on routing; it's main use is to guard on the request's
258    /// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
259    /// See [`Files::guard`] for routing guards.
260    pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
261        self.use_guards = Some(Rc::new(guard));
262        self
263    }
264
265    /// See [`Files::method_guard`].
266    #[doc(hidden)]
267    #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
268    pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
269        self.method_guard(guard)
270    }
271
272    /// Disable `Content-Disposition` header.
273    ///
274    /// By default Content-Disposition` header is enabled.
275    pub fn disable_content_disposition(mut self) -> Self {
276        self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
277        self
278    }
279
280    /// Sets default handler which is used when no matched file could be found.
281    ///
282    /// # Examples
283    /// Setting a fallback static file handler:
284    /// ```
285    /// use actix_files::{Files, NamedFile};
286    /// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
287    ///
288    /// # fn run() -> Result<(), actix_web::Error> {
289    /// let files = Files::new("/", "./static")
290    ///     .index_file("index.html")
291    ///     .default_handler(fn_service(|req: ServiceRequest| async {
292    ///         let (req, _) = req.into_parts();
293    ///         let file = NamedFile::open_async("./static/404.html").await?;
294    ///         let res = file.into_response(&req);
295    ///         Ok(ServiceResponse::new(req, res))
296    ///     }));
297    /// # Ok(())
298    /// # }
299    /// ```
300    pub fn default_handler<F, U>(mut self, f: F) -> Self
301    where
302        F: IntoServiceFactory<U, ServiceRequest>,
303        U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
304            + 'static,
305    {
306        // create and configure default resource
307        self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
308            f.into_factory().map_init_err(|_| ()),
309        )))));
310
311        self
312    }
313
314    /// Enables serving hidden files and directories, allowing a leading dots in url fragments.
315    pub fn use_hidden_files(mut self) -> Self {
316        self.hidden_files = true;
317        self
318    }
319}
320
321impl HttpServiceFactory for Files {
322    fn register(mut self, config: &mut AppService) {
323        let guards = if self.guards.is_empty() {
324            None
325        } else {
326            let guards = std::mem::take(&mut self.guards);
327            Some(
328                guards
329                    .into_iter()
330                    .map(|guard| -> Box<dyn Guard> { Box::new(guard) })
331                    .collect::<Vec<_>>(),
332            )
333        };
334
335        if self.default.borrow().is_none() {
336            *self.default.borrow_mut() = Some(config.default_service());
337        }
338
339        let rdef = if config.is_root() {
340            ResourceDef::root_prefix(&self.mount_path)
341        } else {
342            ResourceDef::prefix(&self.mount_path)
343        };
344
345        config.register_service(rdef, guards, self, None)
346    }
347}
348
349impl ServiceFactory<ServiceRequest> for Files {
350    type Response = ServiceResponse;
351    type Error = Error;
352    type Config = ();
353    type Service = FilesService;
354    type InitError = ();
355    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
356
357    fn new_service(&self, _: ()) -> Self::Future {
358        let mut inner = FilesServiceInner {
359            directory: self.directory.clone(),
360            index: self.index.clone(),
361            show_index: self.show_index,
362            redirect_to_slash: self.redirect_to_slash,
363            default: None,
364            renderer: self.renderer.clone(),
365            mime_override: self.mime_override.clone(),
366            path_filter: self.path_filter.clone(),
367            file_flags: self.file_flags,
368            guards: self.use_guards.clone(),
369            hidden_files: self.hidden_files,
370        };
371
372        if let Some(ref default) = *self.default.borrow() {
373            let fut = default.new_service(());
374            Box::pin(async {
375                match fut.await {
376                    Ok(default) => {
377                        inner.default = Some(default);
378                        Ok(FilesService(Rc::new(inner)))
379                    }
380                    Err(_) => Err(()),
381                }
382            })
383        } else {
384            Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
385        }
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use actix_web::{
392        http::StatusCode,
393        test::{self, TestRequest},
394        App, HttpResponse,
395    };
396
397    use super::*;
398
399    #[actix_web::test]
400    async fn custom_files_listing_renderer() {
401        let srv = test::init_service(
402            App::new().service(
403                Files::new("/", "./tests")
404                    .show_files_listing()
405                    .files_listing_renderer(|dir, req| {
406                        Ok(ServiceResponse::new(
407                            req.clone(),
408                            HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()),
409                        ))
410                    }),
411            ),
412        )
413        .await;
414
415        let req = TestRequest::with_uri("/").to_request();
416        let res = test::call_service(&srv, req).await;
417
418        assert_eq!(res.status(), StatusCode::OK);
419        let body = test::read_body(res).await;
420        let body_str = std::str::from_utf8(&body).unwrap();
421        let actual_path = Path::new(&body_str);
422        let expected_path = Path::new("actix-files/tests");
423        assert!(
424            actual_path.ends_with(expected_path),
425            "body {:?} does not end with {:?}",
426            actual_path,
427            expected_path
428        );
429    }
430}