//go:build !windows package version import ( "fmt" "os" "path/filepath" ) // replaceUpdateFile preserves the old inode under backupPath, then atomically // renames the adjacent replacement over targetPath. At no point is the target // pathname absent. func replaceUpdateFile(targetPath, replacementPath, backupPath string) error { if err := os.Link(targetPath, backupPath); err != nil { return fmt.Errorf("create update backup link: %w", err) } cleanupBackup := true defer func() { if cleanupBackup { _ = os.Remove(backupPath) } }() if err := syncDirectory(filepath.Dir(targetPath)); err != nil { return fmt.Errorf("sync update backup: %w", err) } if err := os.Rename(replacementPath, targetPath); err != nil { return fmt.Errorf("atomically replace update target: %w", err) } cleanupBackup = false if err := syncDirectory(filepath.Dir(targetPath)); err != nil { return fmt.Errorf("sync update replacement: %w", err) } return nil } // restoreUpdateFile atomically renames the known-good backup over the failed // replacement. The primary pathname remains continuously present. func restoreUpdateFile(targetPath, backupPath string) error { if err := os.Rename(backupPath, targetPath); err != nil { return fmt.Errorf("atomically restore update backup: %w", err) } if err := os.Chmod(targetPath, 0o755); err != nil { return fmt.Errorf("restore update target permissions: %w", err) } if err := syncDirectory(filepath.Dir(targetPath)); err != nil { return fmt.Errorf("sync restored update target: %w", err) } return nil } // Callers sync the destination directory after the atomic metadata change. func moveUpdateFileReplacing(sourcePath, targetPath string) error { return os.Rename(sourcePath, targetPath) }