Last active
May 16, 2025 03:32
-
-
Save hajimehoshi/8c4dc3bf668761ddedbbaa9de1ef0268 to your computer and use it in GitHub Desktop.
File atomic writing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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