fix(setup): preserve symlinks in atomicWriteFile

Add ResolveForWrite helper that resolves symlinks before writing,
so atomic writes go to the symlink target instead of replacing
the symlink itself.
This commit is contained in:
Doug Campos
2025-12-20 18:07:48 -05:00
parent 79191bf7d1
commit c7615e4075
4 changed files with 117 additions and 2 deletions

View File

@@ -4,12 +4,20 @@ 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 {
dir := filepath.Dir(path)
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")
@@ -38,7 +46,7 @@ func atomicWriteFile(path string, data []byte) error {
}
// Atomic rename
if err := os.Rename(tmpPath, path); err != nil {
if err := os.Rename(tmpPath, targetPath); err != nil {
_ = os.Remove(tmpPath) // Best effort cleanup
return fmt.Errorf("rename temp file: %w", err)
}

View File

@@ -67,6 +67,45 @@ func TestAtomicWriteFile(t *testing.T) {
}
}
func TestAtomicWriteFile_PreservesSymlink(t *testing.T) {
tmpDir := t.TempDir()
// Create target file
target := filepath.Join(tmpDir, "target.txt")
if err := os.WriteFile(target, []byte("original"), 0644); err != nil {
t.Fatal(err)
}
// Create symlink
link := filepath.Join(tmpDir, "link.txt")
if err := os.Symlink(target, link); err != nil {
t.Fatal(err)
}
// Write via symlink
if err := atomicWriteFile(link, []byte("updated")); err != nil {
t.Fatalf("atomicWriteFile failed: %v", err)
}
// Verify symlink still exists
info, err := os.Lstat(link)
if err != nil {
t.Fatalf("failed to lstat link: %v", err)
}
if info.Mode()&os.ModeSymlink == 0 {
t.Error("symlink was replaced with regular file")
}
// Verify target was updated
data, err := os.ReadFile(target)
if err != nil {
t.Fatalf("failed to read target: %v", err)
}
if string(data) != "updated" {
t.Errorf("target content = %q, want %q", string(data), "updated")
}
}
func TestDirExists(t *testing.T) {
tmpDir := t.TempDir()