package fsutil import ( "errors" "fmt" "os" "path/filepath" "syscall" ) // dirSync is the directory durability hook; tests replace it. var dirSync = syncDirImpl // WriteFileAtomic writes data via temp file + Sync + rename so readers never see a partial file. // After a successful rename the parent directory is opened and Sync'd. func WriteFileAtomic(path string, data []byte, perm os.FileMode) error { return writeFileAtomic(path, data, perm, false) } // WriteFileAtomicExclusive is WriteFileAtomic but creates the destination with O_EXCL. func WriteFileAtomicExclusive(path string, data []byte, perm os.FileMode) error { return writeFileAtomic(path, data, perm, true) } func writeFileAtomic(path string, data []byte, perm os.FileMode, exclusive bool) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return err } if exclusive { f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, perm) if err != nil { if os.IsExist(err) { return fmt.Errorf("fsutil: refuse overwrite of existing %s", path) } return err } if cerr := f.Close(); cerr != nil { return cerr } } tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp") if err != nil { return err } tmpName := tmp.Name() cleanup := true defer func() { if cleanup { _ = os.Remove(tmpName) } }() if _, err := tmp.Write(data); err != nil { _ = tmp.Close() return fmt.Errorf("atomic write: %w", err) } if err := tmp.Sync(); err != nil { _ = tmp.Close() return fmt.Errorf("atomic sync: %w", err) } if err := tmp.Close(); err != nil { return err } if err := os.Chmod(tmpName, perm); err != nil { return err } if err := os.Rename(tmpName, path); err != nil { return err } cleanup = false if err := dirSync(dir); err != nil { if ignorableDirSync(err) { return nil } return err } return nil } // SyncDir fsyncs a directory so create/rename/unlink entries become durable. func SyncDir(dir string) error { return dirSync(dir) } func syncDirImpl(dir string) error { d, err := os.Open(dir) if err != nil { return fmt.Errorf("fsutil: open dir %s: %w", dir, err) } defer d.Close() if err := d.Sync(); err != nil { if ignorableDirSync(err) { return nil } return fmt.Errorf("fsutil: sync dir %s: %w", dir, err) } return nil } func ignorableDirSync(err error) bool { return errors.Is(err, syscall.EINVAL) || errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.ENOSYS) }