diff --git a/tower-http/src/services/fs/serve_dir/mod.rs b/tower-http/src/services/fs/serve_dir/mod.rs index 61b956d1..f4a703c3 100644 --- a/tower-http/src/services/fs/serve_dir/mod.rs +++ b/tower-http/src/services/fs/serve_dir/mod.rs @@ -52,6 +52,7 @@ const DEFAULT_CAPACITY: usize = 65536; #[derive(Clone, Debug)] pub struct ServeDir { base: PathBuf, + prepend_path: String, buf_chunk_size: usize, precompressed_variants: Option, // This is used to specialise implementation for @@ -72,6 +73,7 @@ impl ServeDir { Self { base, + prepend_path: "".to_string(), buf_chunk_size: DEFAULT_CAPACITY, precompressed_variants: None, variant: ServeVariant::Directory { @@ -88,6 +90,7 @@ impl ServeDir { { Self { base: path.as_ref().to_owned(), + prepend_path: "".to_string(), buf_chunk_size: DEFAULT_CAPACITY, precompressed_variants: None, variant: ServeVariant::SingleFile { mime }, @@ -115,6 +118,19 @@ impl ServeDir { } } + /// Sets a path to be prepended when performing a trailing slash redirect. + /// + /// This is useful when you want to serve the files at another location than "/", for example + /// when you are using multiple services and want this instance to handle `/static/`. + /// In that example, you should pass in "/static" so that a trailing slash redirect does not + /// redirect to `//` but instead to `/static//` + /// + /// The default is the empty string. + pub fn prepend_path(mut self, path: String) -> Self { + self.prepend_path = path; + self + } + /// Set a specific read buffer chunk size. /// /// The default capacity is 64kb. @@ -211,6 +227,7 @@ impl ServeDir { /// ``` pub fn fallback(self, new_fallback: F2) -> ServeDir { ServeDir { + prepend_path: "".to_string(), base: self.base, buf_chunk_size: self.buf_chunk_size, precompressed_variants: self.precompressed_variants, @@ -358,6 +375,8 @@ impl ServeDir { } }; + let prepend_path = self.prepend_path.clone(); + let buf_chunk_size = self.buf_chunk_size; let range_header = req .headers() @@ -375,6 +394,7 @@ impl ServeDir { let open_file_future = Box::pin(open_file::open_file( variant, + prepend_path, path_to_file, req, negotiated_encodings, diff --git a/tower-http/src/services/fs/serve_dir/open_file.rs b/tower-http/src/services/fs/serve_dir/open_file.rs index 01c1e2f9..aa6c829d 100644 --- a/tower-http/src/services/fs/serve_dir/open_file.rs +++ b/tower-http/src/services/fs/serve_dir/open_file.rs @@ -40,6 +40,7 @@ pub(super) enum FileRequestExtent { pub(super) async fn open_file( variant: ServeVariant, + prepend_path: String, mut path_to_file: PathBuf, req: Request>, negotiated_encodings: Vec<(Encoding, QValue)>, @@ -64,6 +65,7 @@ pub(super) async fn open_file( // returned which corresponds to a Some(output). Otherwise the path might be // modified and proceed to the open file/metadata future. if let Some(output) = maybe_redirect_or_append_path( + &prepend_path, &mut path_to_file, req.uri(), append_index_html_on_directories, @@ -251,6 +253,7 @@ async fn file_metadata_with_fallback( } async fn maybe_redirect_or_append_path( + prepend_path: &str, path_to_file: &mut PathBuf, uri: &Uri, append_index_html_on_directories: bool, @@ -267,8 +270,10 @@ async fn maybe_redirect_or_append_path( path_to_file.push("index.html"); None } else { - let location = - HeaderValue::from_str(&append_slash_on_path(uri.clone()).to_string()).unwrap(); + let location_string = format!("{prepend_path}{}", append_slash_on_path(uri.clone())); + + let location = HeaderValue::from_str(&location_string).unwrap(); + Some(OpenFileOutput::Redirect { location }) } } diff --git a/tower-http/src/services/fs/serve_dir/tests.rs b/tower-http/src/services/fs/serve_dir/tests.rs index d0d3952c..23e6504f 100644 --- a/tower-http/src/services/fs/serve_dir/tests.rs +++ b/tower-http/src/services/fs/serve_dir/tests.rs @@ -384,6 +384,19 @@ async fn redirect_to_trailing_slash_on_dir() { assert_eq!(location, "/src/"); } +#[tokio::test] +async fn redirect_to_trailing_slash_with_prepend_path() { + let svc = ServeDir::new(".").prepend_path("/foo".to_string()); + + let req = Request::builder().uri("/src").body(Body::empty()).unwrap(); + let res = svc.oneshot(req).await.unwrap(); + + assert_eq!(res.status(), StatusCode::TEMPORARY_REDIRECT); + + let location = &res.headers()[http::header::LOCATION]; + assert_eq!(location, "/foo/src/"); +} + #[tokio::test] async fn empty_directory_without_index() { let svc = ServeDir::new(".").append_index_html_on_directories(false);