atomicWrite writes content from r to path via a temp file in the same directory. If the target exists, its permissions are preserved. On failure the temp file is cleaned up and the original is untouched.
(ctx context.Context, path string, mode *os.FileMode, r io.Reader)
| 567 | // On failure the temp file is cleaned up and the original is |
| 568 | // untouched. |
| 569 | func (api *API) atomicWrite(ctx context.Context, path string, mode *os.FileMode, r io.Reader) (int, error) { |
| 570 | logger := api.logger.With(agentchat.Fields(ctx)...) |
| 571 | |
| 572 | dir := filepath.Dir(path) |
| 573 | tmpName := filepath.Join(dir, fmt.Sprintf(".%s.tmp.%s", filepath.Base(path), uuid.New().String()[:8])) |
| 574 | |
| 575 | tmpfile, err := api.filesystem.OpenFile(tmpName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o666) |
| 576 | if err != nil { |
| 577 | status := http.StatusInternalServerError |
| 578 | if errors.Is(err, os.ErrPermission) { |
| 579 | status = http.StatusForbidden |
| 580 | } |
| 581 | return status, err |
| 582 | } |
| 583 | |
| 584 | cleanup := func() { |
| 585 | if err := api.filesystem.Remove(tmpName); err != nil { |
| 586 | logger.Warn(ctx, "unable to clean up temp file", slog.Error(err)) |
| 587 | } |
| 588 | } |
| 589 | |
| 590 | _, err = io.Copy(tmpfile, r) |
| 591 | if err != nil { |
| 592 | _ = tmpfile.Close() |
| 593 | cleanup() |
| 594 | return http.StatusInternalServerError, xerrors.Errorf("write %s: %w", path, err) |
| 595 | } |
| 596 | |
| 597 | // Close before rename to flush buffered data and catch write |
| 598 | // errors (e.g. delayed allocation failures). |
| 599 | if err := tmpfile.Close(); err != nil { |
| 600 | cleanup() |
| 601 | return http.StatusInternalServerError, xerrors.Errorf("write %s: %w", path, err) |
| 602 | } |
| 603 | |
| 604 | // Set permissions on the temp file before rename so there is |
| 605 | // no window where the target has wrong permissions. |
| 606 | if mode != nil { |
| 607 | if err := api.filesystem.Chmod(tmpName, *mode); err != nil { |
| 608 | logger.Warn(ctx, "unable to set file permissions", |
| 609 | slog.F("path", path), |
| 610 | slog.Error(err), |
| 611 | ) |
| 612 | } |
| 613 | } |
| 614 | |
| 615 | if err := api.filesystem.Rename(tmpName, path); err != nil { |
| 616 | cleanup() |
| 617 | status := http.StatusInternalServerError |
| 618 | if errors.Is(err, os.ErrPermission) { |
| 619 | status = http.StatusForbidden |
| 620 | } |
| 621 | return status, xerrors.Errorf("write %s: %w", path, err) |
| 622 | } |
| 623 | |
| 624 | return 0, nil |
| 625 | } |
| 626 |