package fileutil import ( "fmt" "os" "path/filepath" "syscall" ) type LockedFile struct { file *os.File } func WithLockedFile(path string, fn func() error) error { lock, err := acquireLock(path) if err != nil { return err } defer lock.Close() return fn() } func AtomicWriteFile(path string, data []byte, perm os.FileMode) error { dir := filepath.Dir(path) base := filepath.Base(path) tmp, err := os.CreateTemp(dir, base+".*.tmp") if err != nil { return fmt.Errorf("create tmp: %w", err) } tmpPath := tmp.Name() defer func() { _ = os.Remove(tmpPath) }() if err := tmp.Chmod(perm); err != nil { _ = tmp.Close() return fmt.Errorf("chmod tmp: %w", err) } if _, err := tmp.Write(data); err != nil { _ = tmp.Close() return fmt.Errorf("write tmp: %w", err) } if err := tmp.Sync(); err != nil { _ = tmp.Close() return fmt.Errorf("sync tmp: %w", err) } if err := tmp.Close(); err != nil { return fmt.Errorf("close tmp: %w", err) } if err := os.Rename(tmpPath, path); err != nil { return fmt.Errorf("rename tmp: %w", err) } if err := syncDir(dir); err != nil { return err } return nil } func acquireLock(path string) (*LockedFile, error) { lockPath := path + ".lock" file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return nil, fmt.Errorf("open lock: %w", err) } if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil { _ = file.Close() return nil, fmt.Errorf("lock file: %w", err) } return &LockedFile{file: file}, nil } func (l *LockedFile) Close() error { if l == nil || l.file == nil { return nil } unlockErr := syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN) closeErr := l.file.Close() if unlockErr != nil { return fmt.Errorf("unlock file: %w", unlockErr) } if closeErr != nil { return fmt.Errorf("close lock: %w", closeErr) } return nil } func syncDir(dir string) error { f, err := os.Open(dir) if err != nil { return fmt.Errorf("open dir: %w", err) } defer f.Close() if err := f.Sync(); err != nil { return fmt.Errorf("sync dir: %w", err) } return nil }