From 5f9f91ee2f24062c53d291dc345f94465b5c9c1b Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Mon, 14 Sep 2026 16:02:02 +0800 Subject: [PATCH] fix(blob): fsync blob files via a write handle so Windows works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync sweep (and the EXDEV copy fallback) opened blob files with File::open — a read-only handle — before calling sync_all. POSIX fsync accepts read-only fds, so Linux never noticed, but Windows FlushFileBuffers requires a GENERIC_WRITE handle and fails with ACCESS_DENIED (os error 5) on every call. On Windows deployments the strict sweep therefore failed every deferred sync, and the post-copy fsync silently never happened. Files now open via OpenOptions::write(true); the best-effort directory fsyncs keep the read-only POSIX dirent idiom unchanged. Co-Authored-By: Claude Code --- .../services/local_blob_backend.rs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) 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;