fix(download): fsync staged downloads before the promote rename

The staged-write-then-rename protocol was atomic against process kills but
not against power loss: f2fs/ext4 can persist the rename before the data,
leaving a zero-length or truncated file under the final name. Sync the
staged file before promoting and fsync the directory after.
This commit is contained in:
zarzet
2026-07-11 12:44:19 +07:00
parent 21aeaafced
commit 2192e68684
+18
View File
@@ -333,6 +333,14 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
}
}
// Sync before the promote rename so a power loss right after the rename
// cannot leave a truncated file under the final name.
if err := out.Sync(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to sync file: %v", err),
})
}
if err := out.Close(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
@@ -346,6 +354,7 @@ func (r *extensionRuntime) fileDownload(call goja.FunctionCall) goja.Value {
})
}
promoted = true
syncDir(filepath.Dir(fullPath))
GoLog("[Extension:%s] Downloaded %d bytes to %s\n", r.extensionID, written, fullPath)
@@ -590,6 +599,14 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
}
}
// Sync before the promote rename so a power loss right after the rename
// cannot leave a truncated file under the final name.
if err := out.Sync(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
"error": fmt.Sprintf("failed to sync file: %v", err),
})
}
if err := out.Close(); err != nil {
return r.vm.ToValue(map[string]any{
"success": false,
@@ -603,6 +620,7 @@ func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, full
})
}
promoted = true
syncDir(filepath.Dir(fullPath))
GoLog("[Extension:%s] Chunked download complete: %d bytes to %s\n", r.extensionID, totalWritten, fullPath)