writeJSON atomically writes entries to path as pretty-printed JSON. The new content is written to a sibling temp file, fsync'd, and renamed over the destination. POSIX guarantees the rename is atomic, so a concurrent reader sees either the previous content or the new content in full — never a parti
(path string, entries map[string]string)
| 328 | // in full — never a partial write. The parent directory is fsync'd |
| 329 | // after the rename so the rename itself is durable across an OS crash. |
| 330 | func writeJSON(path string, entries map[string]string) error { |
| 331 | dir := filepath.Dir(path) |
| 332 | if err := os.MkdirAll(dir, 0o700); err != nil { |
| 333 | return fmt.Errorf("creating cache directory %q: %w", dir, err) |
| 334 | } |
| 335 | |
| 336 | data, err := json.MarshalIndent(entries, "", " ") |
| 337 | if err != nil { |
| 338 | return fmt.Errorf("marshaling cache: %w", err) |
| 339 | } |
| 340 | |
| 341 | tmp, err := os.CreateTemp(dir, ".cache-*.json") |
| 342 | if err != nil { |
| 343 | return fmt.Errorf("creating temp cache file: %w", err) |
| 344 | } |
| 345 | tmpName := tmp.Name() |
| 346 | // Cleanup on any error path; harmless once Rename has moved the file. |
| 347 | defer os.Remove(tmpName) |
| 348 | |
| 349 | if _, err := tmp.Write(data); err != nil { |
| 350 | tmp.Close() |
| 351 | return fmt.Errorf("writing temp cache file: %w", err) |
| 352 | } |
| 353 | if err := tmp.Sync(); err != nil { |
| 354 | tmp.Close() |
| 355 | return fmt.Errorf("syncing temp cache file: %w", err) |
| 356 | } |
| 357 | if err := tmp.Close(); err != nil { |
| 358 | return fmt.Errorf("closing temp cache file: %w", err) |
| 359 | } |
| 360 | |
| 361 | if err := os.Rename(tmpName, path); err != nil { |
| 362 | return fmt.Errorf("renaming cache file: %w", err) |
| 363 | } |
| 364 | |
| 365 | syncDir(dir) |
| 366 | return nil |
| 367 | } |
| 368 | |
| 369 | // syncDir best-effort fsyncs a directory so a recent rename inside it is |
| 370 | // persisted. Directory fsync is not portable (e.g. unsupported on |