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