fix(zip): prevent premature temp file deletion during ZIP download

Use NamedTempFile::into_parts() to reuse the existing fd instead of
opening a second one, and store TempPath in response extensions so the
file is only deleted after the body stream finishes.

Before: temp_file was dropped when the handler returned (before Axum
streamed the body). Worked only by accident on Unix (unlinked files
remain readable while an fd is open) but used 2 fds and was fragile.

After: single fd, explicit lifetime guarantee, cross-platform correct.
This commit is contained in:
Dionisio
2026-02-24 12:54:11 +01:00
parent 0986ebf81c
commit 41af7f0933
+10 -21
View File
@@ -446,22 +446,13 @@ impl FolderHandler {
file_size file_size
); );
// Open the temp file with tokio for async streaming // Split the NamedTempFile into the already-open std File
let tokio_file = match tokio::fs::File::open(temp_file.path()).await { // and the TempPath (auto-deletes on drop). This reuses
Ok(f) => f, // the existing fd instead of opening a second one.
Err(e) => { let (std_file, temp_path) = temp_file.into_parts();
tracing::error!("Error opening temp file for streaming: {}", e); let tokio_file = tokio::fs::File::from_std(std_file);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Error streaming ZIP file"
})),
)
.into_response();
}
};
// Stream the temp file to the client in chunks // Stream the file to the client in chunks
let stream = ReaderStream::new(tokio_file); let stream = ReaderStream::new(tokio_file);
let body = axum::body::Body::from_stream(stream); let body = axum::body::Body::from_stream(stream);
@@ -469,7 +460,7 @@ impl FolderHandler {
let filename = format!("{}.zip", folder.name); let filename = format!("{}.zip", folder.name);
let content_disposition = format!("attachment; filename=\"{}\"", filename); let content_disposition = format!("attachment; filename=\"{}\"", filename);
let response = Response::builder() let mut response = Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/zip") .header(header::CONTENT_TYPE, "application/zip")
.header(header::CONTENT_DISPOSITION, content_disposition) .header(header::CONTENT_DISPOSITION, content_disposition)
@@ -477,11 +468,9 @@ impl FolderHandler {
.body(body) .body(body)
.unwrap(); .unwrap();
// temp_file is kept alive until the response future // Keep TempPath alive in the response extensions so the
// completes; dropped afterwards, cleaning up the file. // file is only deleted AFTER the body stream finishes.
// We move it into the response extensions so it lives response.extensions_mut().insert(Arc::new(temp_path));
// long enough for the stream to be fully read.
let _ = temp_file;
response.into_response() response.into_response()
} }