Skip to content

Instantly share code, notes, and snippets.

@hajimehoshi
Last active May 16, 2025 03:32
Show Gist options
  • Save hajimehoshi/8c4dc3bf668761ddedbbaa9de1ef0268 to your computer and use it in GitHub Desktop.
Save hajimehoshi/8c4dc3bf668761ddedbbaa9de1ef0268 to your computer and use it in GitHub Desktop.
File atomic writing
//go:build !android && !ios && !js
package storage
import (
"errors"
"io/fs"
"os"
"path/filepath"
"sync"
)
var fileMutex sync.Mutex
// atomicWriteFile writes the given content to the given path.
// atomicWriteFile creates a file in an atomic manner by renaming.
func atomicWriteFile(path string, content []byte, backup bool) error {
fileMutex.Lock()
defer fileMutex.Unlock()
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
if err := os.WriteFile(path+".tmp", content, 0644); err != nil {
return err
}
// Create a back-up file just in case when updating the file fails.
var backupfile string
if backup {
backupfile = path + ".back"
}
if err := rename(path+".tmp", path, backupfile); err != nil {
return err
}
return nil
}
//go:build !windows
package storage
import (
"errors"
"os"
)
func rename(from, to, backup string) error {
if backup != "" {
if _, err := os.Stat(to); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
backup = ""
}
}
if backup != "" {
if err := os.Rename(to, backup); err != nil {
return err
}
}
if err := os.Rename(from, to); err != nil {
return err
}
return nil
}
package storage
import (
"errors"
"fmt"
"io/fs"
"os"
"runtime"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procReplaceFileW = kernel32.NewProc("ReplaceFileW")
)
func ensureFile(filename string) error {
_, err := os.Stat(filename)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
}
return nil
}
func rename(from, to, backup string) error {
from16, err := windows.UTF16PtrFromString(from)
if err != nil {
return err
}
to16, err := windows.UTF16PtrFromString(to)
if err != nil {
return err
}
var backup16 *uint16
if backup != "" {
ptr, err := windows.UTF16PtrFromString(backup)
if err != nil {
return err
}
backup16 = ptr
}
// Ensure the existence of a file whose name is the to file name.
if err := ensureFile(to); err != nil {
return err
}
// Use ReplaceFileW instead of MoveFileExW, that is used in os.Rename.
// See the discussions:
// * https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/449bb49d-8acc-48dc-a46f-0760ceddbfc3/movefileexmovefilereplaceexisting-ntfs-same-volume-atomic
// * https://stackoverflow.com/questions/167414/is-an-atomic-file-rename-with-overwrite-possible-on-windows
// * https://groups.google.com/g/golang-nuts/c/JFvnLx246uM
//
// Especiallly when a backup file name is specified, ReplaceFileW seems to work as an atomic operation.
// See the API reference:
// * https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew#return-value
const trials = 5
for i := 0; i < trials; i++ {
r, _, e := procReplaceFileW.Call(uintptr(unsafe.Pointer(to16)), uintptr(unsafe.Pointer(from16)), uintptr(unsafe.Pointer(backup16)), 0, 0, 0)
runtime.KeepAlive(from16)
runtime.KeepAlive(to16)
runtime.KeepAlive(backup16)
if int32(r) != 0 {
return nil
}
if errors.Is(e, windows.ERROR_SHARING_VIOLATION) {
time.Sleep(time.Millisecond)
continue
}
return fmt.Errorf("storage: ReplaceFileW failed: %w", e)
}
return fmt.Errorf("storage: ReplaceFileW failed: all %d trials failed", trials)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment