resolve conversation1

This commit is contained in:
lanbinshen 2026-03-01 16:01:24 +08:00
parent c55a37a6da
commit bdd485e965
3 changed files with 174 additions and 73 deletions

View File

@ -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
}
}

View File

@ -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<ServiceRequest> 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;
})
}
}

View File

@ -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",
"<html>Index from test2</html>",
)
.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"<html>Index from test2</html>");
// 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");
}