The workbook is written straight into the temporary file beside the target rather than into a string first, so saving a large workbook doesn't hold the file in memory; the conflict check and rename stay in the background.
- commit
- a50df9d4373ba547d50bc8cd337d0bea2f845e63
- parent
- cc3626c2467468d766d9b1b0d0a97c76015c375f
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 46 | ++++++++++++++++++++---------- |
@@ -2,6 +2,7 @@ package ui
22 33 import (44 "fmt"5+ "io"56 "io/fs"67 "log/slog"78 "os"@@ -189,14 +190,16 @@ func (m *Model) saveAs(name string, check bool) tea.Cmd {
189190 }190191 191192 // saveCmd writes the worksheet to path atomically: to a temporary file192-// first, then renamed over the target. With expect set, it writes only193-// if the file on disk is still that version.193+// beside it first, then renamed over the target. The file is written194+// here, as the workbook stands, streaming (see Workbook.Write), so a195+// large workbook isn't held twice in memory; the rename happens in the196+// background. With expect set, it renames only if the file on disk is197+// still that version.194198 func saveCmd(s *sheet.Sheet, name, path string, expect *stamp, spans *telemetry.Trace) tea.Cmd {195199 span := spans.Start("save")196- var buf strings.Builder197- err := s.Write(&buf)200+ tmp, n, err := writeTemp(path, s.Write)198201 if telemetry.Enabled() {199- span.End(slog.Int("cells", s.Len()), slog.Int("bytes", buf.Len()))202+ span.End(slog.Int("cells", s.Len()), slog.Int64("bytes", n))200203 }201204 return func() tea.Msg {202205 if err != nil {@@ -204,37 +207,50 @@ func saveCmd(s *sheet.Sheet, name, path string, expect *stamp, spans *telemetry.
204207 }205208 // A file deleted meanwhile is simply written again.206209 if now := diskStamp(path); expect != nil && now != (stamp{}) && !now.equal(*expect) {210+ os.Remove(tmp)207211 return savedMsg{name: name, conflict: true}208212 }209- if err := writeAtomic(path, buf.String()); err != nil {213+ if err := os.Rename(tmp, path); err != nil {214+ os.Remove(tmp)210215 return savedMsg{name: name, err: err}211216 }212217 return savedMsg{name: name, stamp: diskStamp(path)}213218 }214219 }215220 216-// writeAtomic writes data to path through a temporary file of its own217-// beside it, so two writers never share one, then renames it over path.218-func writeAtomic(path, data string) error {221+// writeTemp writes a file with write into a temporary file of its own222+// beside path, so two writers never share one, and returns its name and223+// size. On an error it removes the file.224+func writeTemp(path string, write func(io.Writer) error) (string, int64, error) {219225 f, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*.tmp")220226 if err != nil {221- return err227+ return "", 0, err222228 }223229 tmp := f.Name()224- _, err = f.WriteString(data)230+ cw := &countWriter{w: f}231+ err = write(cw)225232 if err == nil {226233 err = f.Chmod(0o644)227234 }228235 if cerr := f.Close(); err == nil {229236 err = cerr230237 }231- if err == nil {232- err = os.Rename(tmp, path)233- }234238 if err != nil {235239 os.Remove(tmp)236240 }237- return err241+ return tmp, cw.n, err242+}243+244+// countWriter counts the bytes written through it.245+type countWriter struct {246+ w io.Writer247+ n int64248+}249+250+func (c *countWriter) Write(p []byte) (int, error) {251+ n, err := c.w.Write(p)252+ c.n += int64(n)253+ return n, err238254 }239255 240256 // loadCmd reads the file in the background, the span of it and its