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:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user