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