Skip to content

Commit d0be2d5

Browse files
fix(assets): flush tokio file after multipart upload to prevent truncated reads
tokio::fs::File::write_all returns as soon as data is copied to an internal buffer and a blocking write task is spawned — it does NOT wait for the blocking write to complete. When the File is dropped without flushing, the last write may still be in-flight. A subsequent fs::read can then see a truncated file. This caused flaky E2E failures in the compositor-image-overlay upload test: the image crate's into_dimensions() would fail with 'unexpected end of file' because it was parsing a partially-written PNG. The plugin upload handler in server/mod.rs already had this fix; apply the same pattern to all asset upload functions (image, audio, font) in assets.rs and plugin_assets.rs. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
1 parent 63250fd commit d0be2d5

File tree

2 files changed

+32
-0
lines changed

2 files changed

+32
-0
lines changed

apps/skit/src/assets.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ async fn write_upload_stream_to_disk(
277277
}
278278
}
279279

280+
// Flush pending writes — tokio::fs::File::write_all returns as soon as
281+
// data is copied to an internal buffer and a blocking write is spawned,
282+
// so the last write may still be in-flight when the File is dropped.
283+
if let Err(e) = file.flush().await {
284+
let _ = fs::remove_file(file_path).await;
285+
return Err(AssetsError::IoError(format!("Failed to flush file: {e}")));
286+
}
287+
280288
// Create default license file (best-effort).
281289
let license_path = file_path.with_extension(format!("{extension}.license"));
282290
// REUSE-IgnoreStart
@@ -710,6 +718,14 @@ async fn write_image_upload_to_disk(
710718
}
711719
}
712720

721+
// Flush pending writes — tokio::fs::File::write_all returns as soon as
722+
// data is copied to an internal buffer and a blocking write is spawned,
723+
// so the last write may still be in-flight when the File is dropped.
724+
if let Err(e) = file.flush().await {
725+
let _ = fs::remove_file(file_path).await;
726+
return Err(AssetsError::IoError(format!("Failed to flush file: {e}")));
727+
}
728+
713729
Ok(total_bytes)
714730
}
715731

@@ -1253,6 +1269,14 @@ async fn write_font_upload_to_disk(
12531269
}
12541270
}
12551271

1272+
// Flush pending writes — tokio::fs::File::write_all returns as soon as
1273+
// data is copied to an internal buffer and a blocking write is spawned,
1274+
// so the last write may still be in-flight when the File is dropped.
1275+
if let Err(e) = file.flush().await {
1276+
let _ = fs::remove_file(file_path).await;
1277+
return Err(AssetsError::IoError(format!("Failed to flush file: {e}")));
1278+
}
1279+
12561280
// Create default license file (best-effort).
12571281
let license_path = file_path.with_extension(format!("{extension}.license"));
12581282
// REUSE-IgnoreStart

apps/skit/src/plugin_assets.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,14 @@ async fn write_upload_to_disk(
836836
}
837837
}
838838

839+
// Flush pending writes — tokio::fs::File::write_all returns as soon as
840+
// data is copied to an internal buffer and a blocking write is spawned,
841+
// so the last write may still be in-flight when the File is dropped.
842+
if let Err(e) = file.flush().await {
843+
let _ = fs::remove_file(file_path).await;
844+
return Err(PluginAssetError::IoError(format!("Failed to flush file: {e}")));
845+
}
846+
839847
Ok(total_bytes)
840848
}
841849

0 commit comments

Comments
 (0)