Add ResolveForWrite helper that resolves symlinks before writing, so atomic writes go to the symlink target instead of replacing the symlink itself.
79 lines
2.0 KiB
Go
79 lines
2.0 KiB
Go
package setup
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/steveyegge/beads/internal/utils"
|
|
)
|
|
|
|
// atomicWriteFile writes data to a file atomically using a unique temporary file.
|
|
// This prevents race conditions when multiple processes write to the same file.
|
|
// If path is a symlink, writes to the resolved target (preserving the symlink).
|
|
func atomicWriteFile(path string, data []byte) error {
|
|
targetPath, err := utils.ResolveForWrite(path)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve path: %w", err)
|
|
}
|
|
|
|
dir := filepath.Dir(targetPath)
|
|
|
|
// Create unique temp file in same directory
|
|
tmpFile, err := os.CreateTemp(dir, ".*.tmp")
|
|
if err != nil {
|
|
return fmt.Errorf("create temp file: %w", err)
|
|
}
|
|
tmpPath := tmpFile.Name()
|
|
|
|
// Write data
|
|
if _, err := tmpFile.Write(data); err != nil {
|
|
_ = tmpFile.Close() // best effort cleanup
|
|
_ = os.Remove(tmpPath) // best effort cleanup
|
|
return fmt.Errorf("write temp file: %w", err)
|
|
}
|
|
|
|
// Close temp file
|
|
if err := tmpFile.Close(); err != nil {
|
|
_ = os.Remove(tmpPath) // Best effort cleanup
|
|
return fmt.Errorf("close temp file: %w", err)
|
|
}
|
|
|
|
// Set permissions to 0600 (owner read/write only)
|
|
if err := os.Chmod(tmpPath, 0600); err != nil {
|
|
_ = os.Remove(tmpPath) // Best effort cleanup
|
|
return fmt.Errorf("set permissions: %w", err)
|
|
}
|
|
|
|
// Atomic rename
|
|
if err := os.Rename(tmpPath, targetPath); err != nil {
|
|
_ = os.Remove(tmpPath) // Best effort cleanup
|
|
return fmt.Errorf("rename temp file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DirExists checks if a directory exists
|
|
func DirExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && info.IsDir()
|
|
}
|
|
|
|
// FileExists checks if a file exists
|
|
func FileExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && !info.IsDir()
|
|
}
|
|
|
|
// EnsureDir creates a directory if it doesn't exist
|
|
func EnsureDir(path string, perm os.FileMode) error {
|
|
if DirExists(path) {
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(path, perm); err != nil {
|
|
return fmt.Errorf("create directory: %w", err)
|
|
}
|
|
return nil
|
|
}
|