|
|
|
@@ -4,6 +4,7 @@ import ( |
|
|
|
"encoding/json" |
|
|
|
"fmt" |
|
|
|
"os" |
|
|
|
"path/filepath" |
|
|
|
"strconv" |
|
|
|
"strings" |
|
|
|
) |
|
|
|
@@ -250,7 +251,34 @@ func Save(path string, cfg Config) error { |
|
|
|
return err |
|
|
|
} |
|
|
|
data = append(data, '\n') |
|
|
|
return os.WriteFile(path, data, 0o644) |
|
|
|
// NEW-1 fix: write to a temp file in the same directory, then rename atomically. |
|
|
|
// A direct os.WriteFile on the target leaves a corrupt file if the process |
|
|
|
// crashes mid-write. os.Rename is atomic on POSIX filesystems. |
|
|
|
dir := filepath.Dir(path) |
|
|
|
tmp, err := os.CreateTemp(dir, ".fmrtx-config-*.json.tmp") |
|
|
|
if err != nil { |
|
|
|
return fmt.Errorf("config save: create temp: %w", err) |
|
|
|
} |
|
|
|
tmpPath := tmp.Name() |
|
|
|
if _, err := tmp.Write(data); err != nil { |
|
|
|
_ = tmp.Close() |
|
|
|
_ = os.Remove(tmpPath) |
|
|
|
return fmt.Errorf("config save: write temp: %w", err) |
|
|
|
} |
|
|
|
if err := tmp.Sync(); err != nil { |
|
|
|
_ = tmp.Close() |
|
|
|
_ = os.Remove(tmpPath) |
|
|
|
return fmt.Errorf("config save: sync temp: %w", err) |
|
|
|
} |
|
|
|
if err := tmp.Close(); err != nil { |
|
|
|
_ = os.Remove(tmpPath) |
|
|
|
return fmt.Errorf("config save: close temp: %w", err) |
|
|
|
} |
|
|
|
if err := os.Rename(tmpPath, path); err != nil { |
|
|
|
_ = os.Remove(tmpPath) |
|
|
|
return fmt.Errorf("config save: rename: %w", err) |
|
|
|
} |
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
func (c Config) Validate() error { |
|
|
|
|