From 41af7f0933b9bdafe0333d5e1d2314d6a8c35ae7 Mon Sep 17 00:00:00 2001 From: Dionisio Date: Tue, 24 Feb 2026 12:54:11 +0100 Subject: [PATCH] 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. --- src/interfaces/api/handlers/folder_handler.rs | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/interfaces/api/handlers/folder_handler.rs b/src/interfaces/api/handlers/folder_handler.rs index bdf92a98..4d8830fa 100644 --- a/src/interfaces/api/handlers/folder_handler.rs +++ b/src/interfaces/api/handlers/folder_handler.rs @@ -446,22 +446,13 @@ impl FolderHandler { file_size ); - // Open the temp file with tokio for async streaming - let tokio_file = match tokio::fs::File::open(temp_file.path()).await { - Ok(f) => f, - Err(e) => { - tracing::error!("Error opening temp file for streaming: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Error streaming ZIP file" - })), - ) - .into_response(); - } - }; + // Split the NamedTempFile into the already-open std File + // and the TempPath (auto-deletes on drop). This reuses + // the existing fd instead of opening a second one. + let (std_file, temp_path) = temp_file.into_parts(); + let tokio_file = tokio::fs::File::from_std(std_file); - // Stream the temp file to the client in chunks + // Stream the file to the client in chunks let stream = ReaderStream::new(tokio_file); let body = axum::body::Body::from_stream(stream); @@ -469,7 +460,7 @@ impl FolderHandler { let filename = format!("{}.zip", folder.name); let content_disposition = format!("attachment; filename=\"{}\"", filename); - let response = Response::builder() + let mut response = Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/zip") .header(header::CONTENT_DISPOSITION, content_disposition) @@ -477,11 +468,9 @@ impl FolderHandler { .body(body) .unwrap(); - // temp_file is kept alive until the response future - // completes; dropped afterwards, cleaning up the file. - // We move it into the response extensions so it lives - // long enough for the stream to be fully read. - let _ = temp_file; + // Keep TempPath alive in the response extensions so the + // file is only deleted AFTER the body stream finishes. + response.extensions_mut().insert(Arc::new(temp_path)); response.into_response() }