remove buffers in file writer
19 files changed, 169 insertions(+), 411 deletions(-)
changed files
- domain/content/builder/builder.go
- domain/content/builder/template/template.go
- domain/content/posts.go
- domain/content/templates/list.go
- domain/content/templates/post.go
- domain/content/templates/tags.go
- domain/vanity/templates/gopkg.go
- domain/web/mux.go
- domain/web/templates/files.go
- domain/web/templates/layout.go
- go.mod
- go.sum
- shared/storage/file.go
- shared/storage/files/file.go
- shared/storage/files/writer.go
- shared/storage/interface.go
M domain/content/builder/builder.go → domain/content/builder/builder.go
@@ -2,13 +2,13 @@ package builder import ( "fmt" - "io" "os" "path" "path/filepath" "slices" "time" + "alin.ovh/gomponents" "alin.ovh/x/log" "github.com/Southclaws/fault" "github.com/Southclaws/fault/fmsg"@@ -20,7 +20,6 @@ ctemplates "alin.ovh/homestead/domain/content/templates" "alin.ovh/homestead/domain/indieweb/sitemap" vtemplates "alin.ovh/homestead/domain/vanity/templates" "alin.ovh/homestead/domain/web/templates" - "alin.ovh/homestead/shared/buffer" "alin.ovh/homestead/shared/config" "alin.ovh/homestead/shared/storage" "alin.ovh/homestead/shared/vcs"@@ -42,18 +41,12 @@ } } func copyFile(storage storage.Writer, src string, rel string) error { - buf := new(buffer.Buffer) - sf, err := os.Open(src) if err != nil { return fault.Wrap(err) } defer sf.Close() - buf.Reset() - if _, err := io.Copy(buf, sf); err != nil { - return fault.Wrap(err) - } - if err := storage.Write("/"+rel, "", buf); err != nil { + if err := storage.Write("/"+rel, "", sf); err != nil { return fault.Wrap(err) }@@ -65,7 +58,6 @@ options *Options, config *config.Config, log *log.Logger, ) error { - buf := new(buffer.Buffer) joinSource := joinSourcePath(options.Source) stor := options.Storage postDir := "post"@@ -96,25 +88,20 @@ for _, post := range cc.Posts { log.Debug("rendering post", "post", post.Basename) sitemap.AddPath(post.URL, post.Date) - buf.Reset() - if err := ctemplates.PostPage(siteSettings, post).Render(buf); err != nil { - return fault.Wrap(err, fmsg.With("could not render post")) - } - if err := stor.WritePost(post, buf); err != nil { + if err := stor.WritePost(post, ctemplates.PostPage(siteSettings, post)); err != nil { return fault.Wrap(err) } } log.Debug("rendering tags list") - buf.Reset() - if err := ctemplates.TagsPage(siteSettings, ctemplates.TagsPageVars{ - Title: "Tags", - Tags: mapset.Sorted(cc.Tags), - }).Render(buf); err != nil { - return fault.Wrap(err) - } - if err := stor.Write("/tags/", "Tags", buf); err != nil { + err = stor.Write("/tags/", "Tags", + ctemplates.TagsPage(siteSettings, ctemplates.TagsPageVars{ + Title: "Tags", + Tags: mapset.Sorted(cc.Tags), + }), + ) + if err != nil { return fault.Wrap(err) } sitemap.AddPath("/tags/", lastMod)@@ -128,14 +115,13 @@ } } log.Debug("rendering tags page", "tag", tag) url := path.Join("/tags", tag) + "/" - buf.Reset() - if err := ctemplates.TagPage(siteSettings, ctemplates.TagPageVars{ - Tag: tag, - Posts: matchingPosts, - }).Render(buf); err != nil { - return fault.Wrap(err) - } - if err = stor.Write(url, tag, buf); err != nil { + err = stor.Write(url, tag, + ctemplates.TagPage(siteSettings, ctemplates.TagPageVars{ + Tag: tag, + Posts: matchingPosts, + }), + ) + if err != nil { return fault.Wrap(err) } sitemap.AddPath(url, matchingPosts[0].Date)@@ -151,29 +137,22 @@ ) if err != nil { return fault.Wrap(err, fmsg.With("could not render tag feed page")) } - buf.Reset() - if _, err := feed.WriteTo(buf); err != nil { - return fault.Wrap(err) - } file := &storage.File{ Title: title, LastModified: matchingPosts[0].Date, Path: path.Join("/tags", tag, "atom.xml"), FSPath: path.Join("/tags", tag, "atom.xml"), } - if err := stor.WriteFile(file, buf); err != nil { + if err := stor.WriteFile(file, feed); err != nil { return fault.Wrap(err) } } log.Debug("rendering list page") - buf.Reset() - if err := ctemplates.ListPage(siteSettings, ctemplates.ListPageVars{ + listPage := ctemplates.ListPage(siteSettings, ctemplates.ListPageVars{ Posts: cc.Posts, - }).Render(buf); err != nil { - return fault.Wrap(err) - } - if err := stor.Write(path.Join("/", postDir)+"/", "Posts", buf); err != nil { + }) + if err := stor.Write(path.Join("/", postDir)+"/", "Posts", listPage); err != nil { return fault.Wrap(err) } sitemap.AddPath(path.Join("/", postDir)+"/", lastMod)@@ -183,17 +162,13 @@ feed, err := template.RenderFeed(config.Title, config, cc.Posts, "feed") if err != nil { return fault.Wrap(err, fmsg.With("could not render feed")) } - buf.Reset() - if _, err := feed.WriteTo(buf); err != nil { - return fault.Wrap(err) - } file := &storage.File{ Title: config.Title, LastModified: cc.Posts[0].Date, Path: "/atom.xml", FSPath: "/atom.xml", } - if err := stor.WriteFile(file, buf); err != nil { + if err := stor.WriteFile(file, feed); err != nil { return fault.Wrap(err) }@@ -205,37 +180,30 @@ for _, e := range tplFiles { filename := e.Name() - buf.Reset() log.Debug("rendering template file", "filename", filename) - if err := template.CopyFile(filename, buf); err != nil { - return fault.Wrap( - err, - fmsg.With(fmt.Sprintf("could not copy file %s", filename)), - ) + f, err := templates.Files.Open(filename) + if err != nil { + return fault.Wrap(err) } - if err := stor.Write("/"+filename, "", buf); err != nil { + if err := stor.Write("/"+filename, "", templates.MakeWriterTo(f)); err != nil { return fault.Wrap(err) } } for _, post := range cc.Pages { - buf.Reset() log.Debug("rendering page", "source", post.Input, "path", post.URL) + var page gomponents.Node if post.URL == "/" { - if err := ctemplates.Homepage(siteSettings, ctemplates.HomepageVars{ + page = ctemplates.Homepage(siteSettings, ctemplates.HomepageVars{ Email: config.PublicEmail, Me: config.RelMe, Posts: cc.Posts, - }, post).Render(buf); err != nil { - return fault.Wrap(err) - } + }, post) } else { - if err := ctemplates.Page(siteSettings, post).Render(buf); err != nil { - return fault.Wrap(err) - } + page = ctemplates.Page(siteSettings, post) } file := stor.NewFileFromPost(post) - if err := stor.WriteFile(file, buf); err != nil { + if err := stor.WriteFile(file, page.(gomponents.NodeWriter)); err != nil { return fault.Wrap(err) } }@@ -244,26 +212,18 @@ // it would be nice to set LastMod here, but using the latest post // date would be wrong as the homepage has its own content file // without a date, which could be newer sitemap.AddPath("/", time.Time{}) - if err := buf.SeekStart(); err != nil { - return fault.Wrap(err) - } log.Debug("rendering sitemap") - buf.Reset() - if _, err := sitemap.WriteTo(buf); err != nil { - return fault.Wrap(err) - } - if err := stor.Write("/sitemap.xml", "sitemap", buf); err != nil { + if err := stor.Write("/sitemap.xml", "sitemap", sitemap); err != nil { return fault.Wrap(err) } log.Debug("rendering robots.txt") - buf.Reset() - err = template.RenderRobotsTXT(config.BaseURL, buf) + robots, err := template.RenderRobotsTXT(config.BaseURL) if err != nil { return fault.Wrap(err) } - if err := stor.Write("/robots.txt", "", buf); err != nil { + if err := stor.Write("/robots.txt", "", robots); err != nil { return fault.Wrap(err) }@@ -278,20 +238,14 @@ } log.Debug("rendering go packages") for _, p := range config.Go.Packages { - buf.Reset() - if err := vtemplates.GoPackagePage(siteSettings, &config.Go, p).Render(buf); err != nil { - return fault.Wrap(err) - } - if err := stor.Write(fmt.Sprintf("/go/%s.html", p), siteSettings.Title, buf); err != nil { + page := vtemplates.GoPackagePage(siteSettings, &config.Go, p) + if err := stor.Write(fmt.Sprintf("/go/%s.html", p), siteSettings.Title, page); err != nil { return fault.Wrap(err) } } - buf.Reset() - if err := vtemplates.GoPackageListPage(siteSettings, &config.Go).Render(buf); err != nil { - return fault.Wrap(err) - } - if err := stor.Write("/go/", siteSettings.Title, buf); err != nil { + page := vtemplates.GoPackageListPage(siteSettings, &config.Go) + if err := stor.Write("/go/", siteSettings.Title, page); err != nil { return fault.Wrap(err) }
M domain/content/builder/template/template.go → domain/content/builder/template/template.go
@@ -7,6 +7,7 @@ "io" "io/fs" "text/template" + "alin.ovh/gomponents" "github.com/Southclaws/fault" "github.com/Southclaws/fault/fmsg"@@ -16,19 +17,22 @@ "alin.ovh/homestead/domain/web/templates" "alin.ovh/homestead/shared/config" ) -func RenderRobotsTXT(baseURL config.URL, w io.Writer) error { +func RenderRobotsTXT(baseURL config.URL) (gomponents.NodeWriter, error) { tpl, err := template.ParseFS(templates.Files, "robots.tmpl") if err != nil { - return fault.Wrap(err) - } - err = tpl.Execute(w, map[string]any{ - "BaseURL": baseURL, - }) - if err != nil { - return fault.Wrap(err) + return nil, fault.Wrap(err) } - return nil + return gomponents.NodeWriterFunc(func(w io.Writer) (int64, error) { + err = tpl.Execute(w, map[string]any{ + "BaseURL": baseURL, + }) + if err != nil { + return 0, fault.Wrap(err) + } + + return 0, nil + }), nil } func RenderFeed(
M domain/content/posts.go → domain/content/posts.go
@@ -89,14 +89,18 @@ return nil } -// implements gomponent.Node func (p *Post) Render(w io.Writer) error { return markdown.Convert(p.content, w) } +// implements [io.WriterTo] +func (p *Post) WriteTo(w io.Writer) (int64, error) { + return 0, markdown.Convert(p.content, w) +} + func (p *Post) RenderString() (string, error) { var buf bytes.Buffer - if err := p.Render(&buf); err != nil { + if _, err := p.WriteTo(&buf); err != nil { return "", fault.Wrap(err, fmsg.With("could not convert markdown content")) }
M domain/content/templates/list.go → domain/content/templates/list.go
@@ -17,7 +17,7 @@ type ListPageVars struct { Posts []*content.Post } -func TagPage(site templates.SiteSettings, vars TagPageVars) g.Node { +func TagPage(site templates.SiteSettings, vars TagPageVars) g.NodeWriter { return templates.Layout(site, templates.PageSettings{ Title: vars.Tag, TitleAttrs: templates.Attrs{"class": "p-author h-card"},@@ -33,7 +33,7 @@ list(vars.Posts), )) } -func ListPage(site templates.SiteSettings, vars ListPageVars) g.Node { +func ListPage(site templates.SiteSettings, vars ListPageVars) g.NodeWriter { return templates.Layout(site, templates.PageSettings{ Title: site.Title, TitleAttrs: templates.Attrs{"class": "p-author h-card"},
M domain/content/templates/post.go → domain/content/templates/post.go
@@ -11,7 +11,7 @@ "alin.ovh/homestead/domain/web/templates" "alin.ovh/homestead/shared/vcs" ) -func PostPage(site templates.SiteSettings, post *content.Post) g.Node { +func PostPage(site templates.SiteSettings, post *content.Post) g.NodeWriter { return templates.Layout(site, templates.PageSettings{ Title: post.Title, TitleAttrs: templates.Attrs{"class": "p-author h-card", "rel": "author"},
M domain/vanity/templates/gopkg.go → domain/vanity/templates/gopkg.go
@@ -17,7 +17,7 @@ Domain config.URL Forge config.URL } -func GoPackageListPage(site templates.SiteSettings, cfg *config.GoPackagesConfig) g.Node { +func GoPackageListPage(site templates.SiteSettings, cfg *config.GoPackagesConfig) g.NodeWriter { return templates.Layout(site, templates.PageSettings{ Title: site.Title, TitleAttrs: templates.Attrs{"class": "p-author h-card"},@@ -43,7 +43,11 @@ ), ) } -func GoPackagePage(site templates.SiteSettings, cfg *config.GoPackagesConfig, pkg string) g.Node { +func GoPackagePage( + site templates.SiteSettings, + cfg *config.GoPackagesConfig, + pkg string, +) g.NodeWriter { return templates.Layout(site, templates.PageSettings{ Title: site.Title, TitleAttrs: templates.Attrs{"class": "p-author h-card"},
M domain/web/mux.go → domain/web/mux.go
@@ -68,10 +68,9 @@ enc := nego.NegotiateContentEncoding(r, file.AvailableEncodings()...) if enc != "" { w.Header().Add("Content-Encoding", enc) } - mime := file.GetContentType() - w.Header().Add("Content-Type", mime) + w.Header().Add("Content-Type", file.ContentType) - if mime == "application/xml" { + if file.ContentType == "application/xml" { for k, v := range feedHeaders { w.Header().Add(k, v) }
M domain/web/templates/files.go → domain/web/templates/files.go
@@ -1,14 +1,27 @@ package templates import ( + "io" "io/fs" "os" "alin.ovh/homestead/shared/env" ) +type FileWriterTo struct { + File fs.File +} + var Files fs.FS func init() { Files = os.DirFS(env.GetEnvFallback("KO_DATA_PATH", "kodata")) } + +func (f FileWriterTo) WriteTo(w io.Writer) (int64, error) { + return io.Copy(w, f.File) +} + +func MakeWriterTo(file fs.File) io.WriterTo { + return &FileWriterTo{file} +}
M domain/web/templates/layout.go → domain/web/templates/layout.go
@@ -50,7 +50,7 @@ return g.Attr(k, v) // can't inline this because it uses ...value, grr }) } -func Layout(site SiteSettings, page PageSettings, children ...g.Node) g.Node { +func Layout(site SiteSettings, page PageSettings, children ...g.Node) g.NodeWriter { return DoctypeHTML(FormattedDocument( HTML( Lang(site.Language),@@ -115,8 +115,8 @@ g.Text(item.Name), ) } -func FormattedDocument(doc g.Node) g.Node { - return g.NodeFunc(func(w io.Writer) error { +func FormattedDocument(doc g.Node) g.NodeWriter { + return g.NodeWriterFunc(func(w io.Writer) (int64, error) { pr, pw := io.Pipe() defer pr.Close()@@ -125,19 +125,23 @@ pw.CloseWithError(doc.Render(pw)) }() if err := htmlformat.Document(w, pr); err != nil { - return fault.Wrap(err) + return 0, fault.Wrap(err) } - return nil + return 0, nil }) } -func DoctypeHTML(sibling g.Node) g.Node { - return g.NodeFunc(func(w io.Writer) error { +func DoctypeHTML(sibling g.Node) g.NodeWriter { + return g.NodeWriterFunc(func(w io.Writer) (int64, error) { if _, err := w.Write([]byte("<!doctype html>\n")); err != nil { - return err + return 0, err } - return sibling.Render(w) + if sibling, ok := sibling.(g.NodeWriter); ok { + return sibling.WriteTo(w) + } + + return 0, sibling.Render(w) }) }
M go.sum → go.sum
@@ -1,7 +1,7 @@ 9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q= 9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM= -alin.ovh/gomponents v1.6.0 h1:ymFYykOwMSG63dQ8bQZ4U0611zkDEzVNMOpKQSwwFjE= -alin.ovh/gomponents v1.6.0/go.mod h1:RQtPwKzGB0Nt1JTBDSqRzH2w/mg/6PkuhnNvQMV0/AQ= +alin.ovh/gomponents v1.8.0 h1:KE5y9yRRe/H8se4BVqcc86gNSHqVs/eyIqWDf79m9nc= +alin.ovh/gomponents v1.8.0/go.mod h1:RQtPwKzGB0Nt1JTBDSqRzH2w/mg/6PkuhnNvQMV0/AQ= alin.ovh/x v1.2.0 h1:YhI0zB4kRS86ClS1QzOaqhIg79ov0vRXAGBE1MfIr/w= alin.ovh/x v1.2.0/go.mod h1:L1xZ3Dn5v2Iw9zkktaYHhJelMT1MnBbO+sfyoyvzK+Q= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
D shared/buffer/buffer.go
@@ -1,108 +0,0 @@ -package buffer - -import ( - "io" -) - -type Buffer struct { - buf []byte - pos int - len int -} - -const blockSize = 512 - -func NewBuffer(buf []byte) *Buffer { - return &Buffer{ - buf: buf, - pos: 0, - len: len(buf), - } -} - -// Read implements io.Reader's Read method -// Return normal error for special io.EOF handling -func (b *Buffer) Read(p []byte) (int, error) { - if b.pos >= b.len { - return 0, io.EOF - } - - n := len(p) - if n > b.len-b.pos { - n = b.len - b.pos - } - - copy(p[:n], b.buf[b.pos:b.pos+n]) - b.pos += n - - return n, nil -} - -// Write appends the contents of p to the buffer's data. -func (b *Buffer) Write(p []byte) (int, error) { - if len(b.buf) < b.len+len(p) { - newLen := b.len + len(p) - if cap(b.buf) >= newLen { - b.buf = b.buf[:newLen] - } else { - newBuf := make([]byte, newLen*2) - copy(newBuf, b.buf[:b.len]) - b.buf = newBuf - } - } - - copy(b.buf[b.len:], p) - b.len += len(p) - - return len(p), nil -} - -func (b *Buffer) Len() int { - return b.len -} - -// Reset resets the buffer to be empty. The underlying array is reused if possible. -func (b *Buffer) Reset() { - b.len = 0 - b.pos = 0 -} - -func (b *Buffer) SeekStart() error { - _, err := b.Seek(0, io.SeekStart) - - return err -} - -// Seek moves the read position by offset bytes relative to whence (Start, Current, End) -func (b *Buffer) Seek(offset int64, whence int) (int64, error) { - var newpos int - - switch whence { - case io.SeekStart: - newpos = int(offset) - case io.SeekCurrent: - newpos = b.pos + int(offset) - case io.SeekEnd: - newpos = b.len + int(offset) - default: - return 0, io.EOF - } - - if newpos < 0 { - newpos = 0 - } else if newpos > b.len { - newpos = b.len - } - - b.pos = newpos - - return int64(newpos), nil -} - -func (b *Buffer) Bytes() []byte { - return b.buf[:b.len] -} - -func (b *Buffer) FirstBlock() []byte { - return b.buf[:min(b.len, blockSize)] -}
D shared/buffer/buffer_test.go
@@ -1,101 +0,0 @@ -package buffer - -import ( - "io" - "testing" -) - -func TestWrite(t *testing.T) { - t.Parallel() - b := Buffer{} - data := []byte("test") - - n, err := b.Write(data) - if n != len(data) || err != nil { - t.Errorf("Write failed: expected %d bytes, got %d, error: %v", len(data), n, err) - } - - if b.Len() != len(data) { - t.Errorf("Len is incorrect after write: expected %d, got %d", len(data), b.Len()) - } - - if string(b.Bytes()) != "test" { - t.Error("Bytes returned after write do not match written data") - } -} - -func TestRead(t *testing.T) { - t.Parallel() - b := NewBuffer([]byte("testdata")) - p := make([]byte, 3) - n, err := b.Read(p) - - if n != 3 || string(p[:n]) != "tes" || err != nil { - t.Errorf("Read returned incorrect data: expected 'tes', got '%s', error: %v", p[:n], err) - } - - b.Reset() - data := []byte("abc") - n, err = b.Write(data) - if n != 3 || err != nil { - t.Errorf("Write failed: expected %d bytes, got %d, error: %v", len(data), n, err) - } - - p = make([]byte, 2) - n, err = b.Read(p) - - if n != 2 || string(p) != "ab" || err != nil { - t.Errorf("Read after reset failed: expected 'ab', got '%s', error: %v", p, err) - } - - b.pos = b.len - n, err = b.Read(p) - if n != 0 || err != io.EOF { - t.Errorf("Reading beyond buffer did not return EOF: n=%d, err=%v", n, err) - } -} - -func TestReset(t *testing.T) { - t.Parallel() - b := NewBuffer([]byte("test")) - data := []byte("data") - n, err := b.Write(data) - if n != 4 || err != nil { - t.Errorf("Write failed: expected %d bytes, got %d, error: %v", len(data), n, err) - } - - if b.Len() != 8 || b.pos != 0 { - t.Errorf("Initial buffer state incorrect: len=%d, pos=%d", b.Len(), b.pos) - } - - b.Reset() - if b.Len() != 0 || b.pos != 0 { - t.Errorf("Reset did not clear buffer correctly: len=%d, pos=%d", b.Len(), b.pos) - } -} - -func TestSeek(t *testing.T) { - t.Parallel() - b := NewBuffer([]byte("test")) - tests := []struct { - offset int64 - whence int - expect int64 - err error - }{ - {2, io.SeekStart, 2, nil}, - {-1, io.SeekCurrent, 1, nil}, - {-2, io.SeekEnd, int64(len("test") - 2), nil}, - {5, io.SeekStart, int64(len("test")), nil}, - {-10, io.SeekEnd, 0, nil}, - {0, 999, 0, io.EOF}, // Invalid whence test - } - - for _, tt := range tests { - pos, err := b.Seek(tt.offset, tt.whence) - if pos != tt.expect || (err != tt.err && (err != nil || tt.err != nil)) { - t.Errorf("Seek(%d, %d): expected %d with error %v, got %d and %v", - tt.offset, tt.whence, tt.expect, tt.err, pos, err) - } - } -}
D shared/buffer/writecloser.go
@@ -1,36 +0,0 @@ -package buffer - -import ( - "io" - - "github.com/Southclaws/fault" -) - -type WriteCloser struct { - writers []io.WriteCloser - io.Writer -} - -func NewWriteCloser(writers ...io.WriteCloser) *WriteCloser { - ws := make([]io.Writer, len(writers)) - for i, w := range writers { - ws[i] = io.Writer(w) - } - - return &WriteCloser{ - writers: writers, - Writer: io.MultiWriter(ws...), - } -} - -func (mw *WriteCloser) Close() error { - var lastErr error - for _, w := range mw.writers { - err := w.Close() - if err != nil { - lastErr = err - } - } - - return fault.Wrap(lastErr) -}