diff --git a/src/infrastructure/services/local_blob_backend.rs b/src/infrastructure/services/local_blob_backend.rs index d089f1ad..93f33c5b 100644 --- a/src/infrastructure/services/local_blob_backend.rs +++ b/src/infrastructure/services/local_blob_backend.rs @@ -102,7 +102,23 @@ async fn fsync_paths_parallel(paths: Vec, strict: bool) -> Result<(), D tasks.push(tokio::task::spawn_blocking( move || -> Result<(), (PathBuf, std::io::Error)> { for path in &group { - let result = std::fs::File::open(path).and_then(|f| f.sync_all()); + // `strict` marks blob *file* fsyncs; best-effort marks + // prefix *directory* fsyncs. That distinction also picks + // the open mode: Windows `FlushFileBuffers` needs a + // GENERIC_WRITE handle and fails with ACCESS_DENIED on + // the read-only handle `File::open` returns (POSIX fsync + // accepts read-only fds, which is why this only surfaced + // on Windows). Directories keep the read-only POSIX + // dirent-sync idiom — they can't be fsync'd on Windows + // at all, and their failures stay best-effort warnings. + let result = if strict { + std::fs::OpenOptions::new() + .write(true) + .open(path) + .and_then(|f| f.sync_all()) + } else { + std::fs::File::open(path).and_then(|f| f.sync_all()) + }; if let Err(e) = result { if strict { return Err((path.clone(), e)); @@ -394,7 +410,11 @@ impl BlobStorageBackend for LocalBlobBackend { format!("Failed to copy file to blob store: {}", ce), ) })?; - if let Ok(f) = fs::File::open(&blob_path).await { + // Open for write: Windows `FlushFileBuffers` requires a + // GENERIC_WRITE handle — the read-only handle from + // `File::open` fails with ACCESS_DENIED, silently + // skipping this fsync on every Windows deployment. + if let Ok(f) = fs::OpenOptions::new().write(true).open(&blob_path).await { let _ = f.sync_all().await; } let _ = fs::remove_file(&source_path).await;