cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Save streams into the temporary file

Dami committed a50df9d
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>
1 file changed, +31 -15
Minternal/ui/file.go46++++++++++++++++++++----------
M internal/ui/file.go +31 -15
@@ -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