From bdd485e965a7ec4cc602fb93cc1d10ee9fdefab0 Mon Sep 17 00:00:00 2001 From: lanbinshen Date: Sun, 1 Mar 2026 16:01:24 +0800 Subject: [PATCH] resolve conversation1 --- actix-files/src/files.rs | 1 + actix-files/src/service.rs | 148 +++++++++++++++++----------------- actix-files/tests/encoding.rs | 98 ++++++++++++++++++++++ 3 files changed, 174 insertions(+), 73 deletions(-) diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index e72c1a1bb..d0078464d 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -151,6 +151,7 @@ impl Files { Ok(canon_dir) => canon_dir, Err(_) => { log::error!("Specified path is not a directory: {:?}", orig_dir); + // Preserve original path so requests don't fall back to CWD. orig_dir } } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index da1aa7307..547e5f0b2 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -115,9 +115,9 @@ impl FilesService { /// Show index listing for a directory. /// - /// Uses the directory where the path was found as the base directory for index listing. - fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse { - let dir = Directory::new(path.clone(), path); + /// Uses the provided base directory for calculating relative paths in the index listing. + fn show_index(&self, req: ServiceRequest, base: PathBuf, path: PathBuf) -> ServiceResponse { + let dir = Directory::new(base, path); let (req, _) = req.into_parts(); @@ -175,91 +175,93 @@ impl Service for FilesService { } // Try to find file in multiple directories - let mut found_path = None; let mut last_err = None; for directory in &this.directories { let path = directory.join(&path_on_disk); - match path.canonicalize() { - Ok(_) => { - found_path = Some(path); - break; - } - Err(err) => { - // Keep track of the last error - last_err = Some(err); - } - } - } - let path = match found_path { - Some(path) => path, - None => { - // If all directories failed, use the last error - let err = last_err.unwrap_or_else(|| { - io::Error::new(io::ErrorKind::NotFound, "File not found") - }); - return this.handle_err(err, req).await; - } - }; - - // Try serving pre-compressed file even if the uncompressed file doesn't exist yet. - // Still handle directories (index/listing) through the normal branch below. - if this.try_compressed && !path.is_dir() { - if let Some((named_file, encoding)) = find_compressed(&req, &path).await { - return Ok(this.serve_named_file_with_encoding(req, named_file, encoding)); - } - } - - if path.is_dir() { - if this.redirect_to_slash - && !req.path().ends_with('/') - && (this.index.is_some() || this.show_index) - { - let redirect_to = format!("{}/", req.path()); - - let response = if this.with_permanent_redirect { - HttpResponse::PermanentRedirect() - } else { - HttpResponse::TemporaryRedirect() - } - .insert_header((header::LOCATION, redirect_to)) - .finish(); - - return Ok(req.into_response(response)); - } - - match this.index { - Some(ref index) => { - let named_path = path.join(index); - if this.try_compressed { - if let Some((named_file, encoding)) = - find_compressed(&req, &named_path).await + // Try serving pre-compressed file even if the uncompressed file doesn't exist yet. + // Still handle directories (index/listing) through the normal branch below. + if this.try_compressed { + if let Ok(metadata) = path.metadata() { + if !metadata.is_dir() { + if let Some((named_file, encoding)) = find_compressed(&req, &path).await { return Ok( this.serve_named_file_with_encoding(req, named_file, encoding) ); } } - // fallback to the uncompressed version - match NamedFile::open_async(named_path).await { - Ok(named_file) => Ok(this.serve_named_file(req, named_file)), - Err(_) if this.show_index => Ok(this.show_index(req, path)), - Err(err) => this.handle_err(err, req).await, + } + } + + // Check if path is a directory + if path.is_dir() { + // Handle directory logic inline to avoid multiple iterations + if this.redirect_to_slash + && !req.path().ends_with('/') + && (this.index.is_some() || this.show_index) + { + let redirect_to = format!("{}/", req.path()); + + let response = if this.with_permanent_redirect { + HttpResponse::PermanentRedirect() + } else { + HttpResponse::TemporaryRedirect() + } + .insert_header((header::LOCATION, redirect_to)) + .finish(); + + return Ok(req.into_response(response)); + } + + match this.index { + Some(ref index) => { + let named_path = path.join(index); + if this.try_compressed { + if let Some((named_file, encoding)) = + find_compressed(&req, &named_path).await + { + return Ok(this.serve_named_file_with_encoding( + req, named_file, encoding, + )); + } + } + // fallback to the uncompressed version + match NamedFile::open_async(named_path).await { + Ok(named_file) => return Ok(this.serve_named_file(req, named_file)), + Err(_) if this.show_index => { + return Ok(this.show_index(req, directory.clone(), path)) + } + Err(err) => last_err = Some(err), + } + } + None => { + // No index file configured, check if we should show directory listing + if this.show_index { + return Ok(this.show_index(req, directory.clone(), path)); + } + return Ok(ServiceResponse::from_err( + FilesError::IsDirectory, + req.into_parts().0, + )); + } + } + } else { + // Try to open the file + match NamedFile::open_async(&path).await { + Ok(named_file) => return Ok(this.serve_named_file(req, named_file)), + Err(err) => { + last_err = Some(err); } } - None if this.show_index => Ok(this.show_index(req, path)), - None => Ok(ServiceResponse::from_err( - FilesError::IsDirectory, - req.into_parts().0, - )), - } - } else { - match NamedFile::open_async(&path).await { - Ok(named_file) => Ok(this.serve_named_file(req, named_file)), - Err(err) => this.handle_err(err, req).await, } } + + // If all directories failed, use the last error + let err = last_err + .unwrap_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not found")); + return this.handle_err(err, req).await; }) } } diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index fe3e18caa..1310e1d69 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -272,3 +272,101 @@ async fn test_multiple_directories_iterator() { let _ = std::fs::remove_dir_all("./tests/test3"); let _ = std::fs::remove_dir_all("./tests/test4"); } + +#[actix_web::test] +async fn test_multiple_directories_with_index_file() { + // Create test directories + std::fs::create_dir_all("./tests/test_index1").unwrap(); + std::fs::create_dir_all("./tests/test_index2").unwrap(); + + // Create test files - only second directory has index.html + std::fs::write("./tests/test_index1/other.txt", "Other file").unwrap(); + std::fs::write( + "./tests/test_index2/index.html", + "Index from test2", + ) + .unwrap(); + + // Test multiple directories with index_file - index.html only exists in second directory + let srv = test::init_service( + App::new().service( + Files::new_from_array("/", &["./tests/test_index1", "./tests/test_index2"]) + .index_file("index.html"), + ), + ) + .await; + + // Request / should find index.html in second directory + let req = TestRequest::with_uri("/").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + let body = test::read_body(res).await; + assert_eq!(&body[..], b"Index from test2"); + + // Clean up + let _ = std::fs::remove_dir_all("./tests/test_index1"); + let _ = std::fs::remove_dir_all("./tests/test_index2"); +} + +#[actix_web::test] +async fn test_multiple_directories_try_compressed() { + use actix_web::body::MessageBody; + + // Create test directories + std::fs::create_dir_all("./tests/test_compress1").unwrap(); + std::fs::create_dir_all("./tests/test_compress2").unwrap(); + + // Create test files: + // - First directory has only uncompressed file + // - Second directory has both uncompressed and compressed files + std::fs::copy("./tests/utf8.txt", "./tests/test_compress1/utf8.txt").unwrap(); + std::fs::copy("./tests/utf8.txt", "./tests/test_compress2/other.txt").unwrap(); + std::fs::copy("./tests/utf8.txt.gz", "./tests/test_compress2/other.txt.gz").unwrap(); + + let other_txt_gz_len = std::fs::metadata("./tests/test_compress2/other.txt.gz") + .unwrap() + .len(); + + // Test multiple directories with try_compressed + let srv = test::init_service( + App::new().service( + Files::new_from_array("/", &["./tests/test_compress1", "./tests/test_compress2"]) + .try_compressed(), + ), + ) + .await; + + // Request /utf8.txt - first directory has it uncompressed, should return uncompressed + let mut req = TestRequest::with_uri("/utf8.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("gzip"), + ); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + // First directory has utf8.txt but no .gz version, so no content-encoding + assert_eq!(res.headers().get(header::CONTENT_ENCODING), None); + + // Request /other.txt - second directory has both, should return compressed + let mut req = TestRequest::with_uri("/other.txt").to_request(); + req.headers_mut().insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("gzip"), + ); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_ENCODING), + Some(&HeaderValue::from_static("gzip")), + ); + assert_eq!( + res.into_body().size(), + actix_web::body::BodySize::Sized(other_txt_gz_len), + ); + + // Clean up + let _ = std::fs::remove_dir_all("./tests/test_compress1"); + let _ = std::fs::remove_dir_all("./tests/test_compress2"); +}