diff --git a/activityPub.go b/activityPub.go index 45ebc3b..7b1c181 100644 --- a/activityPub.go +++ b/activityPub.go @@ -22,6 +22,7 @@ import ( "github.com/go-fed/httpsig" "github.com/google/uuid" "github.com/spf13/cast" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -89,7 +90,10 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) { return } apIri := a.apIri(blog) - b, _ := json.Marshal(map[string]any{ + // Encode + buf := bufferpool.Get() + defer bufferpool.Put(buf) + if err := xml.NewEncoder(buf).Encode(map[string]any{ "subject": a.webfingerAccts[apIri], "aliases": []string{ a.webfingerAccts[apIri], @@ -107,9 +111,12 @@ func (a *goBlog) apHandleWebfinger(w http.ResponseWriter, r *http.Request) { "href": apIri, }, }, - }) + }); err != nil { + a.serveError(w, r, "Encoding failed", http.StatusInternalServerError) + return + } w.Header().Set(contentType, "application/jrd+json"+contenttype.CharsetUtf8Suffix) - _, _ = a.min.Write(w, contenttype.JSON, b) + _ = a.min.Get().Minify(contenttype.JSON, w, buf) } func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) { diff --git a/activityStreams.go b/activityStreams.go index 148b366..60dcfb6 100644 --- a/activityStreams.go +++ b/activityStreams.go @@ -10,6 +10,7 @@ import ( "github.com/araddon/dateparse" ct "github.com/elnormous/contenttype" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -90,9 +91,14 @@ type asEndpoints struct { } func (a *goBlog) serveActivityStreamsPost(p *post, w http.ResponseWriter) { - b, _ := json.Marshal(a.toASNote(p)) + buf := bufferpool.Get() + defer bufferpool.Put(buf) + if err := json.NewEncoder(buf).Encode(a.toASNote(p)); err != nil { + http.Error(w, "Encoding failed", http.StatusInternalServerError) + return + } w.Header().Set(contentType, contenttype.ASUTF8) - _, _ = a.min.Write(w, contenttype.AS, b) + _ = a.min.Get().Minify(contenttype.AS, w, buf) } func (a *goBlog) toASNote(p *post) *asNote { @@ -195,7 +201,13 @@ func (a *goBlog) serveActivityStreams(blog string, w http.ResponseWriter, r *htt URL: a.cfg.User.Picture, } } - jb, _ := json.Marshal(asBlog) + // Encode + buf := bufferpool.Get() + defer bufferpool.Put(buf) + if err := json.NewEncoder(buf).Encode(asBlog); err != nil { + a.serveError(w, r, "Encoding failed", http.StatusInternalServerError) + return + } w.Header().Set(contentType, contenttype.ASUTF8) - _, _ = a.min.Write(w, contenttype.AS, jb) + a.min.Get().Minify(contenttype.AS, w, buf) } diff --git a/blogroll.go b/blogroll.go index e40e85a..0f53f18 100644 --- a/blogroll.go +++ b/blogroll.go @@ -3,7 +3,6 @@ package main import ( "bytes" "context" - "io" "log" "net/http" "sort" @@ -54,20 +53,17 @@ func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) { } opmlBuf := bufferpool.Get() defer bufferpool.Put(opmlBuf) - mw := a.min.Writer(contenttype.XML, opmlBuf) - err = opml.Render(mw, &opml.OPML{ + if err = opml.Render(opmlBuf, &opml.OPML{ Version: "2.0", DateCreated: time.Now().UTC(), Outlines: outlines.([]*opml.Outline), - }) - _ = mw.Close() - if err != nil { + }); err != nil { log.Printf("Failed to render OPML: %v", err) a.serveError(w, r, "", http.StatusInternalServerError) return } w.Header().Set(contentType, contenttype.XMLUTF8) - _, _ = io.Copy(w, opmlBuf) + _ = a.min.Get().Minify(contenttype.XML, w, opmlBuf) } func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) { diff --git a/editor.go b/editor.go index d81f7a8..5578e44 100644 --- a/editor.go +++ b/editor.go @@ -138,7 +138,7 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) { a.serveError(w, r, err.Error(), http.StatusBadRequest) return } - gpx, err := io.ReadAll(a.min.Reader(contenttype.XML, file)) + gpx, err := io.ReadAll(a.min.Get().Reader(contenttype.XML, file)) if err != nil { a.serveError(w, r, err.Error(), http.StatusBadRequest) return diff --git a/feeds.go b/feeds.go index 9b793ec..be28171 100644 --- a/feeds.go +++ b/feeds.go @@ -74,6 +74,6 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r _ = pipeWriter.CloseWithError(writeErr) }() w.Header().Set(contentType, feedMediaType+contenttype.CharsetUtf8Suffix) - minifyErr := a.min.Minify(feedMediaType, w, pipeReader) + minifyErr := a.min.Get().Minify(feedMediaType, w, pipeReader) _ = pipeReader.CloseWithError(minifyErr) } diff --git a/httpFs.go b/httpFs.go index d839ca7..9bc5a73 100644 --- a/httpFs.go +++ b/httpFs.go @@ -18,15 +18,15 @@ func (a *goBlog) serveFs(f fs.FS, basePath string) http.HandlerFunc { a.serve404(w, r) return } - var read io.Reader = file switch path.Ext(fileName) { case ".js": w.Header().Set(contentType, contenttype.JSUTF8) - read = a.min.Reader(contenttype.JS, read) + _ = a.min.Get().Minify(contenttype.JS, w, file) case ".css": w.Header().Set(contentType, contenttype.CSSUTF8) - read = a.min.Reader(contenttype.CSS, read) + _ = a.min.Get().Minify(contenttype.CSS, w, file) + default: + _, _ = io.Copy(w, file) } - _, _ = io.Copy(w, read) } } diff --git a/indieAuthServer.go b/indieAuthServer.go index fffff1d..aabd800 100644 --- a/indieAuthServer.go +++ b/indieAuthServer.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/hacdias/indieauth/v2" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -137,9 +138,14 @@ func (a *goBlog) indieAuthVerification(w http.ResponseWriter, r *http.Request, w resp.Token = token resp.Scope = strings.Join(data.Scopes, " ") } - b, _ := json.Marshal(resp) + buf := bufferpool.Get() + defer bufferpool.Put(buf) + if err = json.NewEncoder(buf).Encode(resp); err != nil { + a.serveError(w, r, "Encoding failed", http.StatusInternalServerError) + return + } w.Header().Set(contentType, contenttype.JSONUTF8) - _, _ = a.min.Write(w, contenttype.JSON, b) + _ = a.min.Get().Minify(contenttype.JSON, w, buf) } // Save the authorization request and return the code @@ -196,9 +202,14 @@ func (a *goBlog) indieAuthTokenVerification(w http.ResponseWriter, r *http.Reque Me: a.getFullAddress("") + "/", // MUST contain a path component / trailing slash ClientID: data.ClientID, } + buf := bufferpool.Get() + defer bufferpool.Put(buf) + if err = json.NewEncoder(buf).Encode(res); err != nil { + a.serveError(w, r, "Encoding failed", http.StatusInternalServerError) + return + } w.Header().Set(contentType, contenttype.JSONUTF8) - b, _ := json.Marshal(res) - _, _ = a.min.Write(w, contenttype.JSON, b) + _ = a.min.Get().Minify(contenttype.JSON, w, buf) } // Checks the database for the token and returns the indieAuthData with client and scope. diff --git a/micropub.go b/micropub.go index 0e71676..e90f3ba 100644 --- a/micropub.go +++ b/micropub.go @@ -73,15 +73,12 @@ func (a *goBlog) serveMicropubQuery(w http.ResponseWriter, r *http.Request) { } buf := bufferpool.Get() defer bufferpool.Put(buf) - mw := a.min.Writer(contenttype.JSON, buf) - err := json.NewEncoder(mw).Encode(result) - _ = mw.Close() - if err != nil { + if err := json.NewEncoder(buf).Encode(result); err != nil { a.serveError(w, r, "Failed to encode json", http.StatusInternalServerError) return } w.Header().Set(contentType, contenttype.JSONUTF8) - _, _ = io.Copy(w, buf) + _ = a.min.Get().Minify(contenttype.JSON, w, buf) } func (a *goBlog) serveMicropubPost(w http.ResponseWriter, r *http.Request) { diff --git a/micropub_test.go b/micropub_test.go index a52dfde..bf5e4e7 100644 --- a/micropub_test.go +++ b/micropub_test.go @@ -38,22 +38,22 @@ func Test_micropubQuery(t *testing.T) { testCases := []testCase{ { query: "config", - want: "{\"media-endpoint\":\"http://localhost:8080/micropub/media\"}\n", + want: "{\"media-endpoint\":\"http://localhost:8080/micropub/media\"}", wantStatus: http.StatusOK, }, { query: "source&url=http://localhost:8080/test/post", - want: "{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}\n", + want: "{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}", wantStatus: http.StatusOK, }, { query: "source", - want: "{\"items\":[{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}]}\n", + want: "{\"items\":[{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"]}}]}", wantStatus: http.StatusOK, }, { query: "category", - want: "{\"categories\":[\"test\",\"test2\"]}\n", + want: "{\"categories\":[\"test\",\"test2\"]}", wantStatus: http.StatusOK, }, { diff --git a/nodeinfo.go b/nodeinfo.go index 543c455..bd6d14d 100644 --- a/nodeinfo.go +++ b/nodeinfo.go @@ -25,7 +25,7 @@ func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) { return } w.Header().Set(contentType, contenttype.JSONUTF8) - mw := a.min.Writer(contenttype.JSON, w) + mw := a.min.Get().Writer(contenttype.JSON, w) _, _ = io.Copy(mw, buf) _ = mw.Close() } @@ -36,7 +36,7 @@ func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) { }) buf := bufferpool.Get() defer bufferpool.Put(buf) - err := json.NewEncoder(buf).Encode(map[string]any{ + if err := json.NewEncoder(buf).Encode(map[string]any{ "version": "2.1", "software": map[string]any{ "name": "goblog", @@ -54,13 +54,10 @@ func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) { "webmention", }, "metadata": map[string]any{}, - }) - if err != nil { + }); err != nil { a.serveError(w, r, "", http.StatusInternalServerError) return } w.Header().Set(contentType, contenttype.JSONUTF8) - mw := a.min.Writer(contenttype.JSON, w) - _, _ = io.Copy(mw, buf) - _ = mw.Close() + _ = a.min.Get().Minify(contenttype.JSON, w, buf) } diff --git a/opensearch.go b/opensearch.go index 3008794..5eedcc5 100644 --- a/opensearch.go +++ b/opensearch.go @@ -2,9 +2,9 @@ package main import ( "encoding/xml" - "io" "net/http" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -35,7 +35,6 @@ func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) { _, b := a.getBlog(r) title := a.renderMdTitle(b.Title) sURL := a.getFullAddress(b.getRelativePath(defaultIfEmpty(b.Search.Path, defaultSearchPath))) - w.Header().Set(contentType, "application/opensearchdescription+xml"+contenttype.CharsetUtf8Suffix) openSearch := &openSearchDescription{ ShortName: title, Description: title, @@ -50,10 +49,15 @@ func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) { }, SearchForm: sURL, } - mw := a.min.Writer(contenttype.XML, w) - _, _ = io.WriteString(mw, xml.Header) - _ = xml.NewEncoder(mw).Encode(openSearch) - _ = mw.Close() + buf := bufferpool.Get() + defer bufferpool.Put(buf) + _, _ = buf.WriteString(xml.Header) + if err := xml.NewEncoder(buf).Encode(openSearch); err != nil { + a.serveError(w, r, "", http.StatusInternalServerError) + return + } + w.Header().Set(contentType, "application/opensearchdescription+xml"+contenttype.CharsetUtf8Suffix) + _ = a.min.Get().Minify(contenttype.XML, w, buf) } func openSearchUrl(b *configBlog) string { diff --git a/pkgs/minify/minify.go b/pkgs/minify/minify.go index 2c0c11c..96487b2 100644 --- a/pkgs/minify/minify.go +++ b/pkgs/minify/minify.go @@ -1,7 +1,6 @@ package minify import ( - "io" "sync" "github.com/tdewolff/minify/v2" @@ -21,12 +20,18 @@ type Minifier struct { func (m *Minifier) init() { m.i.Do(func() { m.m = minify.New() + // HTML m.m.AddFunc(contenttype.HTML, mHtml.Minify) + // CSS m.m.AddFunc(contenttype.CSS, mCss.Minify) - m.m.AddFunc(contenttype.XML, mXml.Minify) + // JS m.m.AddFunc(contenttype.JS, mJs.Minify) + // XML + m.m.AddFunc(contenttype.XML, mXml.Minify) m.m.AddFunc(contenttype.RSS, mXml.Minify) m.m.AddFunc(contenttype.ATOM, mXml.Minify) + // JSON + m.m.AddFunc(contenttype.JSON, mJson.Minify) m.m.AddFunc(contenttype.JSONFeed, mJson.Minify) m.m.AddFunc(contenttype.AS, mJson.Minify) }) @@ -36,21 +41,3 @@ func (m *Minifier) Get() *minify.M { m.init() return m.m } - -func (m *Minifier) Write(w io.Writer, mediatype string, b []byte) (int, error) { - mw := m.Writer(mediatype, w) - defer mw.Close() - return mw.Write(b) -} - -func (m *Minifier) Minify(mediatype string, w io.Writer, r io.Reader) error { - return m.Get().Minify(mediatype, w, r) -} - -func (m *Minifier) Writer(mediatype string, w io.Writer) io.WriteCloser { - return m.Get().Writer(mediatype, w) -} - -func (m *Minifier) Reader(mediatype string, r io.Reader) io.Reader { - return m.Get().Reader(mediatype, r) -} diff --git a/render.go b/render.go index c247e7c..0b68979 100644 --- a/render.go +++ b/render.go @@ -41,13 +41,10 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st // Render pipeReader, pipeWriter := io.Pipe() go func() { - minifyWriter := a.min.Writer(contenttype.HTML, pipeWriter) - f(newHtmlBuilder(minifyWriter), data) - _ = minifyWriter.Close() + f(newHtmlBuilder(pipeWriter), data) _ = pipeWriter.Close() }() - _, readErr := io.Copy(w, pipeReader) - _ = pipeReader.CloseWithError(readErr) + _ = pipeReader.CloseWithError(a.min.Get().Minify(contenttype.HTML, w, pipeReader)) } func (a *goBlog) checkRenderData(r *http.Request, data *renderData) { diff --git a/sitemap.go b/sitemap.go index 4857e2a..283027f 100644 --- a/sitemap.go +++ b/sitemap.go @@ -3,12 +3,12 @@ package main import ( "database/sql" "encoding/xml" - "io" "net/http" "time" "github.com/araddon/dateparse" "github.com/snabb/sitemap" + "go.goblog.app/app/pkgs/bufferpool" "go.goblog.app/app/pkgs/contenttype" ) @@ -20,7 +20,7 @@ const ( sitemapBlogPostsPath = "/sitemap-blog-posts.xml" ) -func (a *goBlog) serveSitemap(w http.ResponseWriter, _ *http.Request) { +func (a *goBlog) serveSitemap(w http.ResponseWriter, r *http.Request) { // Create sitemap sm := sitemap.NewSitemapIndex() // Add blog sitemap indices @@ -32,7 +32,7 @@ func (a *goBlog) serveSitemap(w http.ResponseWriter, _ *http.Request) { }) } // Write sitemap - a.writeSitemapXML(w, sm) + a.writeSitemapXML(w, r, sm) } func (a *goBlog) serveSitemapBlog(w http.ResponseWriter, r *http.Request) { @@ -54,7 +54,7 @@ func (a *goBlog) serveSitemapBlog(w http.ResponseWriter, r *http.Request) { LastMod: &now, }) // Write sitemap - a.writeSitemapXML(w, sm) + a.writeSitemapXML(w, r, sm) } func (a *goBlog) serveSitemapBlogFeatures(w http.ResponseWriter, r *http.Request) { @@ -103,7 +103,7 @@ func (a *goBlog) serveSitemapBlogFeatures(w http.ResponseWriter, r *http.Request }) } // Write sitemap - a.writeSitemapXML(w, sm) + a.writeSitemapXML(w, r, sm) } func (a *goBlog) serveSitemapBlogArchives(w http.ResponseWriter, r *http.Request) { @@ -145,7 +145,7 @@ func (a *goBlog) serveSitemapBlogArchives(w http.ResponseWriter, r *http.Request }) } // Write sitemap - a.writeSitemapXML(w, sm) + a.writeSitemapXML(w, r, sm) } // Serve sitemap with all the blog's posts @@ -169,24 +169,22 @@ func (a *goBlog) serveSitemapBlogPosts(w http.ResponseWriter, r *http.Request) { sm.Add(item) } // Write sitemap - a.writeSitemapXML(w, sm) + a.writeSitemapXML(w, r, sm) } -func (a *goBlog) writeSitemapXML(w http.ResponseWriter, sm any) { +func (a *goBlog) writeSitemapXML(w http.ResponseWriter, r *http.Request, sm any) { + buf := bufferpool.Get() + defer bufferpool.Put(buf) + _, _ = buf.WriteString(xml.Header) + _, _ = buf.WriteString(``) + if err := xml.NewEncoder(buf).Encode(sm); err != nil { + a.serveError(w, r, "Failed to encode sitemap", http.StatusInternalServerError) + return + } w.Header().Set(contentType, contenttype.XMLUTF8) - pipeReader, pipeWriter := io.Pipe() - go func() { - mw := a.min.Writer(contenttype.XML, pipeWriter) - _, _ = io.WriteString(mw, xml.Header) - _, _ = io.WriteString(mw, ``) - writeErr := xml.NewEncoder(mw).Encode(sm) - _ = mw.Close() - _ = pipeWriter.CloseWithError(writeErr) - }() - _, copyErr := io.Copy(w, pipeReader) - _ = pipeReader.CloseWithError(copyErr) + _ = a.min.Get().Minify(contenttype.XML, w, buf) } const sitemapDatePathsSql = ` diff --git a/templateAssets.go b/templateAssets.go index e07930c..17bac4f 100644 --- a/templateAssets.go +++ b/templateAssets.go @@ -53,11 +53,11 @@ func (a *goBlog) compileAsset(name string, read io.Reader) error { ext := path.Ext(name) switch ext { case ".js": - read = a.min.Reader(contenttype.JS, read) + read = a.min.Get().Reader(contenttype.JS, read) case ".css": - read = a.min.Reader(contenttype.CSS, read) + read = a.min.Get().Reader(contenttype.CSS, read) case ".xml", ".xsl": - read = a.min.Reader(contenttype.XML, read) + read = a.min.Get().Reader(contenttype.XML, read) } // Read file hash := sha256.New()