mirror of https://github.com/jlelse/GoBlog
Various small improvements and fixes
This commit is contained in:
parent
e994e5400d
commit
7fe07014f5
|
@ -50,13 +50,13 @@ func (a *goBlog) serveBlogrollExport(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, contenttype.XMLUTF8)
|
w.Header().Set(contentType, contenttype.XMLUTF8)
|
||||||
var opmlBytes bytes.Buffer
|
var opmlBuffer bytes.Buffer
|
||||||
_ = opml.Render(&opmlBytes, &opml.OPML{
|
_ = opml.Render(&opmlBuffer, &opml.OPML{
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
DateCreated: time.Now().UTC(),
|
DateCreated: time.Now().UTC(),
|
||||||
Outlines: outlines.([]*opml.Outline),
|
Outlines: outlines.([]*opml.Outline),
|
||||||
})
|
})
|
||||||
_, _ = a.min.Write(w, contenttype.XML, opmlBytes.Bytes())
|
_ = a.min.Minify(contenttype.XML, w, &opmlBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
|
||||||
|
|
40
cache.go
40
cache.go
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -57,11 +56,7 @@ func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Do checks
|
// Do checks
|
||||||
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
if !cacheable(r) {
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false" {
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -71,6 +66,7 @@ func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler {
|
||||||
setLoggedIn(r, false)
|
setLoggedIn(r, false)
|
||||||
} else {
|
} else {
|
||||||
if a.isLoggedIn(r) {
|
if a.isLoggedIn(r) {
|
||||||
|
// Don't cache logged in requests
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -82,11 +78,8 @@ func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler {
|
||||||
return a.cache.getCache(key, next, r), nil
|
return a.cache.getCache(key, next, r), nil
|
||||||
})
|
})
|
||||||
ci := cacheInterface.(*cacheItem)
|
ci := cacheInterface.(*cacheItem)
|
||||||
// copy cached headers
|
// copy and set headers
|
||||||
for k, v := range ci.header {
|
a.cache.setHeaders(w, ci)
|
||||||
w.Header()[k] = v
|
|
||||||
}
|
|
||||||
a.cache.setCacheHeaders(w, ci)
|
|
||||||
// check conditional request
|
// check conditional request
|
||||||
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag {
|
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag {
|
||||||
// send 304
|
// send 304
|
||||||
|
@ -107,6 +100,16 @@ func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cacheable(r *http.Request) bool {
|
||||||
|
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func cacheKey(r *http.Request) string {
|
func cacheKey(r *http.Request) string {
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
// Special cases
|
// Special cases
|
||||||
|
@ -126,7 +129,12 @@ func cacheKey(r *http.Request) string {
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
|
func (c *cache) setHeaders(w http.ResponseWriter, cache *cacheItem) {
|
||||||
|
// Copy headers
|
||||||
|
for k, v := range cache.header.Clone() {
|
||||||
|
w.Header()[k] = v
|
||||||
|
}
|
||||||
|
// Set cache headers
|
||||||
w.Header().Set("ETag", cache.eTag)
|
w.Header().Set("ETag", cache.eTag)
|
||||||
w.Header().Set("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
|
w.Header().Set("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
|
||||||
if w.Header().Get("Cache-Control") == "" {
|
if w.Header().Get("Cache-Control") == "" {
|
||||||
|
@ -163,7 +171,7 @@ func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *
|
||||||
if item == nil {
|
if item == nil {
|
||||||
// No cache available
|
// No cache available
|
||||||
// Make and use copy of r
|
// Make and use copy of r
|
||||||
cr := r.Clone(r.Context())
|
cr := r.Clone(valueOnlyContext{r.Context()})
|
||||||
// Remove problematic headers
|
// Remove problematic headers
|
||||||
cr.Header.Del("If-Modified-Since")
|
cr.Header.Del("If-Modified-Since")
|
||||||
cr.Header.Del("If-Unmodified-Since")
|
cr.Header.Del("If-Unmodified-Since")
|
||||||
|
@ -180,9 +188,7 @@ func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *
|
||||||
_ = result.Body.Close()
|
_ = result.Body.Close()
|
||||||
eTag := result.Header.Get("ETag")
|
eTag := result.Header.Get("ETag")
|
||||||
if eTag == "" {
|
if eTag == "" {
|
||||||
h := sha256.New()
|
eTag, _ = getSHA256(bytes.NewReader(body))
|
||||||
_, _ = io.Copy(h, bytes.NewReader(body))
|
|
||||||
eTag = fmt.Sprintf("%x", h.Sum(nil))
|
|
||||||
}
|
}
|
||||||
lastMod := time.Now()
|
lastMod := time.Now()
|
||||||
if lm := result.Header.Get("Last-Modified"); lm != "" {
|
if lm := result.Header.Get("Last-Modified"); lm != "" {
|
||||||
|
@ -190,12 +196,12 @@ func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *
|
||||||
lastMod = parsedTime
|
lastMod = parsedTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exp, _ := cr.Context().Value(cacheExpirationKey).(int)
|
|
||||||
// Remove problematic headers
|
// Remove problematic headers
|
||||||
result.Header.Del("Accept-Ranges")
|
result.Header.Del("Accept-Ranges")
|
||||||
result.Header.Del("ETag")
|
result.Header.Del("ETag")
|
||||||
result.Header.Del("Last-Modified")
|
result.Header.Del("Last-Modified")
|
||||||
// Create cache item
|
// Create cache item
|
||||||
|
exp, _ := cr.Context().Value(cacheExpirationKey).(int)
|
||||||
item = &cacheItem{
|
item = &cacheItem{
|
||||||
expiration: exp,
|
expiration: exp,
|
||||||
creationTime: lastMod,
|
creationTime: lastMod,
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -12,7 +10,3 @@ func createDefaultTestConfig(t *testing.T) *config {
|
||||||
c.Db.File = filepath.Join(t.TempDir(), "blog.db")
|
c.Db.File = filepath.Join(t.TempDir(), "blog.db")
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func reqWithDefaultBlog(req *http.Request) *http.Request {
|
|
||||||
return req.WithContext(context.WithValue(req.Context(), blogKey, "default"))
|
|
||||||
}
|
|
||||||
|
|
14
editor.go
14
editor.go
|
@ -150,17 +150,19 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
|
||||||
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gpx, err := io.ReadAll(file)
|
originalGpx, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var gpxBuffer bytes.Buffer
|
minifiedGpx, err := a.min.MinifyString(contenttype.XML, string(originalGpx))
|
||||||
_, _ = a.min.Write(&gpxBuffer, contenttype.XML, gpx)
|
if err != nil {
|
||||||
resultMap := map[string]string{
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
"gpx": gpxBuffer.String(),
|
return
|
||||||
}
|
}
|
||||||
resultBytes, err := yaml.Marshal(resultMap)
|
resultBytes, err := yaml.Marshal(map[string]string{
|
||||||
|
"gpx": minifiedGpx,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|
14
feeds.go
14
feeds.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -49,25 +50,26 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
var feedString, feedMediaType string
|
var feedBuffer bytes.Buffer
|
||||||
|
var feedMediaType string
|
||||||
switch f {
|
switch f {
|
||||||
case rssFeed:
|
case rssFeed:
|
||||||
feedMediaType = contenttype.RSS
|
feedMediaType = contenttype.RSS
|
||||||
feedString, err = feed.ToRss()
|
err = feed.WriteRss(&feedBuffer)
|
||||||
case atomFeed:
|
case atomFeed:
|
||||||
feedMediaType = contenttype.ATOM
|
feedMediaType = contenttype.ATOM
|
||||||
feedString, err = feed.ToAtom()
|
err = feed.WriteAtom(&feedBuffer)
|
||||||
case jsonFeed:
|
case jsonFeed:
|
||||||
feedMediaType = contenttype.JSONFeed
|
feedMediaType = contenttype.JSONFeed
|
||||||
feedString, err = feed.ToJSON()
|
err = feed.WriteJSON(&feedBuffer)
|
||||||
default:
|
default:
|
||||||
|
a.serve404(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Header().Del(contentType)
|
|
||||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set(contentType, feedMediaType+contenttype.CharsetUtf8Suffix)
|
w.Header().Set(contentType, feedMediaType+contenttype.CharsetUtf8Suffix)
|
||||||
_, _ = a.min.Write(w, feedMediaType, []byte(feedString))
|
_ = a.min.Minify(feedMediaType, w, &feedBuffer)
|
||||||
}
|
}
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -33,7 +33,7 @@ require (
|
||||||
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9
|
github.com/kaorimatz/go-opml v0.0.0-20210201121027-bc8e2852d7f9
|
||||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||||
github.com/lopezator/migrator v0.3.0
|
github.com/lopezator/migrator v0.3.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.9
|
github.com/mattn/go-sqlite3 v1.14.10
|
||||||
github.com/microcosm-cc/bluemonday v1.0.16
|
github.com/microcosm-cc/bluemonday v1.0.16
|
||||||
github.com/mmcdole/gofeed v1.1.3
|
github.com/mmcdole/gofeed v1.1.3
|
||||||
github.com/paulmach/go.geojson v1.4.0
|
github.com/paulmach/go.geojson v1.4.0
|
||||||
|
@ -44,7 +44,7 @@ require (
|
||||||
github.com/spf13/cast v1.4.1
|
github.com/spf13/cast v1.4.1
|
||||||
github.com/spf13/viper v1.10.1
|
github.com/spf13/viper v1.10.1
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/tdewolff/minify/v2 v2.9.24
|
github.com/tdewolff/minify/v2 v2.9.26
|
||||||
github.com/thoas/go-funk v0.9.1
|
github.com/thoas/go-funk v0.9.1
|
||||||
github.com/tkrajina/gpxgo v1.1.2
|
github.com/tkrajina/gpxgo v1.1.2
|
||||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -303,8 +303,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
|
||||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
||||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
|
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
|
||||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||||
|
@ -400,8 +400,8 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ
|
||||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||||
github.com/tdewolff/minify/v2 v2.9.24 h1:4wlbX+U5IgVa8fH//mlwzv+2g47MN7lu3s9HClAHc28=
|
github.com/tdewolff/minify/v2 v2.9.26 h1:eQy5DRs7vH5pxkF0xtFv29bOgyBEL2vInj4v1bFkK88=
|
||||||
github.com/tdewolff/minify/v2 v2.9.24/go.mod h1:L/bwPtsU/Xx30MxCndlClCMMiLbqROgkR4vZT+QIGXA=
|
github.com/tdewolff/minify/v2 v2.9.26/go.mod h1:L/bwPtsU/Xx30MxCndlClCMMiLbqROgkR4vZT+QIGXA=
|
||||||
github.com/tdewolff/parse/v2 v2.5.26 h1:a/q3lwDCi4GIQ+sSbs4UOHuObhqp8GHAhfqop/zDyQQ=
|
github.com/tdewolff/parse/v2 v2.5.26 h1:a/q3lwDCi4GIQ+sSbs4UOHuObhqp8GHAhfqop/zDyQQ=
|
||||||
github.com/tdewolff/parse/v2 v2.5.26/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
github.com/tdewolff/parse/v2 v2.5.26/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||||
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -11,14 +12,15 @@ func (a *goBlog) serveOpenSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
_, b := a.getBlog(r)
|
_, b := a.getBlog(r)
|
||||||
title := a.renderMdTitle(b.Title)
|
title := a.renderMdTitle(b.Title)
|
||||||
sURL := a.getFullAddress(b.getRelativePath(defaultIfEmpty(b.Search.Path, defaultSearchPath)))
|
sURL := a.getFullAddress(b.getRelativePath(defaultIfEmpty(b.Search.Path, defaultSearchPath)))
|
||||||
xml := fmt.Sprintf("<?xml version=\"1.0\"?><OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\" xmlns:moz=\"http://www.mozilla.org/2006/browser/search/\">"+
|
var buf bytes.Buffer
|
||||||
|
_, _ = fmt.Fprintf(&buf, "<?xml version=\"1.0\"?><OpenSearchDescription xmlns=\"http://a9.com/-/spec/opensearch/1.1/\" xmlns:moz=\"http://www.mozilla.org/2006/browser/search/\">"+
|
||||||
"<ShortName>%s</ShortName><Description>%s</Description>"+
|
"<ShortName>%s</ShortName><Description>%s</Description>"+
|
||||||
"<Url type=\"text/html\" method=\"post\" template=\"%s\"><Param name=\"q\" value=\"{searchTerms}\" /></Url>"+
|
"<Url type=\"text/html\" method=\"post\" template=\"%s\"><Param name=\"q\" value=\"{searchTerms}\" /></Url>"+
|
||||||
"<moz:SearchForm>%s</moz:SearchForm>"+
|
"<moz:SearchForm>%s</moz:SearchForm>"+
|
||||||
"</OpenSearchDescription>",
|
"</OpenSearchDescription>",
|
||||||
title, title, sURL, sURL)
|
title, title, sURL, sURL)
|
||||||
w.Header().Set(contentType, "application/opensearchdescription+xml")
|
w.Header().Set(contentType, "application/opensearchdescription+xml")
|
||||||
_, _ = a.min.Write(w, contenttype.XML, []byte(xml))
|
_ = a.min.Minify(contenttype.XML, w, &buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openSearchUrl(b *configBlog) string {
|
func openSearchUrl(b *configBlog) string {
|
||||||
|
|
|
@ -50,3 +50,7 @@ func (m *Minifier) MinifyBytes(mediatype string, b []byte) ([]byte, error) {
|
||||||
func (m *Minifier) MinifyString(mediatype string, s string) (string, error) {
|
func (m *Minifier) MinifyString(mediatype string, s string) (string, error) {
|
||||||
return m.Get().String(mediatype, s)
|
return m.Get().String(mediatype, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Minifier) Minify(mediatype string, w io.Writer, r io.Reader) error {
|
||||||
|
return m.Get().Minify(mediatype, w, r)
|
||||||
|
}
|
||||||
|
|
10
render.go
10
render.go
|
@ -135,16 +135,14 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st
|
||||||
a.checkRenderData(r, data)
|
a.checkRenderData(r, data)
|
||||||
// Set content type
|
// Set content type
|
||||||
w.Header().Set(contentType, contenttype.HTMLUTF8)
|
w.Header().Set(contentType, contenttype.HTMLUTF8)
|
||||||
// Minify and write response
|
// Render template and write minified HTML
|
||||||
var tw bytes.Buffer
|
var templateBuffer bytes.Buffer
|
||||||
err := a.templates[template].ExecuteTemplate(&tw, template, data)
|
if err := a.templates[template].ExecuteTemplate(&templateBuffer, template, data); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
_, err = a.min.Write(w, contenttype.HTML, tw.Bytes())
|
if err := a.min.Minify(contenttype.HTML, w, &templateBuffer); err != nil {
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,7 +186,7 @@ func (a *goBlog) writeSitemapXML(w http.ResponseWriter, sm interface{}) {
|
||||||
buf.WriteString(a.assetFileName("sitemap.xsl"))
|
buf.WriteString(a.assetFileName("sitemap.xsl"))
|
||||||
buf.WriteString(`" ?>`)
|
buf.WriteString(`" ?>`)
|
||||||
_ = xml.NewEncoder(&buf).Encode(sm)
|
_ = xml.NewEncoder(&buf).Encode(sm)
|
||||||
_, _ = a.min.Write(w, contenttype.XML, buf.Bytes())
|
_ = a.min.Minify(contenttype.XML, w, &buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sitemapDatePathsSql = `
|
const sitemapDatePathsSql = `
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"bytes"
|
||||||
"fmt"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -26,10 +26,18 @@ func (a *goBlog) initTemplateAssets() (err error) {
|
||||||
a.assetFiles = map[string]*assetFile{}
|
a.assetFiles = map[string]*assetFile{}
|
||||||
err = filepath.Walk(assetsFolder, func(path string, info os.FileInfo, err error) error {
|
err = filepath.Walk(assetsFolder, func(path string, info os.FileInfo, err error) error {
|
||||||
if info.Mode().IsRegular() {
|
if info.Mode().IsRegular() {
|
||||||
compiled, err := a.compileAsset(path)
|
// Open file
|
||||||
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Compile asset and close file
|
||||||
|
compiled, err := a.compileAsset(path, file)
|
||||||
|
_ = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Add to map
|
||||||
if compiled != "" {
|
if compiled != "" {
|
||||||
a.assetFileNames[strings.TrimPrefix(path, assetsFolder+"/")] = compiled
|
a.assetFileNames[strings.TrimPrefix(path, assetsFolder+"/")] = compiled
|
||||||
}
|
}
|
||||||
|
@ -47,43 +55,39 @@ func (a *goBlog) initTemplateAssets() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) compileAsset(name string) (string, error) {
|
func (a *goBlog) compileAsset(name string, read io.Reader) (string, error) {
|
||||||
content, err := os.ReadFile(name)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
ext := path.Ext(name)
|
ext := path.Ext(name)
|
||||||
compiledExt := ext
|
compiledExt := ext
|
||||||
|
var contentBuffer bytes.Buffer
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".js":
|
case ".js":
|
||||||
content, err = a.min.MinifyBytes(contenttype.JS, content)
|
if err := a.min.Minify(contenttype.JS, &contentBuffer, read); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
case ".css":
|
case ".css":
|
||||||
content, err = a.min.MinifyBytes(contenttype.CSS, content)
|
if err := a.min.Minify(contenttype.CSS, &contentBuffer, read); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
case ".xml", ".xsl":
|
case ".xml", ".xsl":
|
||||||
content, err = a.min.MinifyBytes(contenttype.XML, content)
|
if err := a.min.Minify(contenttype.XML, &contentBuffer, read); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Do nothing
|
if _, err := io.Copy(&contentBuffer, read); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Hashes
|
// Hashes
|
||||||
sha1Hash := sha1.New()
|
hash, err := getSHA256(bytes.NewReader(contentBuffer.Bytes()))
|
||||||
if _, err := sha1Hash.Write(content); err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// File name
|
// File name
|
||||||
compiledFileName := fmt.Sprintf("%x", sha1Hash.Sum(nil)) + compiledExt
|
compiledFileName := hash + compiledExt
|
||||||
// Create struct
|
// Create struct
|
||||||
a.assetFiles[compiledFileName] = &assetFile{
|
a.assetFiles[compiledFileName] = &assetFile{
|
||||||
contentType: mime.TypeByExtension(compiledExt),
|
contentType: mime.TypeByExtension(compiledExt),
|
||||||
body: content,
|
body: contentBuffer.Bytes(),
|
||||||
}
|
}
|
||||||
return compiledFileName, err
|
return compiledFileName, err
|
||||||
}
|
}
|
||||||
|
@ -114,8 +118,9 @@ func (a *goBlog) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) initChromaCSS() error {
|
func (a *goBlog) initChromaCSS() error {
|
||||||
|
chromaPath := "css/chroma.css"
|
||||||
// Check if file already exists
|
// Check if file already exists
|
||||||
if _, ok := a.assetFiles["css/chroma.css"]; ok {
|
if _, ok := a.assetFiles[chromaPath]; ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Initialize the style
|
// Initialize the style
|
||||||
|
@ -124,25 +129,17 @@ func (a *goBlog) initChromaCSS() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Create a temporary file
|
// Write the CSS to a buffer
|
||||||
chromaTempFile, err := os.CreateTemp("", "chroma-*.css")
|
var cssBuffer bytes.Buffer
|
||||||
if err != nil {
|
if err = chromahtml.New(chromahtml.ClassPrefix("c-")).WriteCSS(&cssBuffer, chromaStyle); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
chromaTempFileName := chromaTempFile.Name()
|
|
||||||
// Write the CSS to the file
|
|
||||||
err = chromahtml.New(chromahtml.ClassPrefix("c-")).WriteCSS(chromaTempFile, chromaStyle)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Close the file
|
|
||||||
_ = chromaTempFile.Close()
|
|
||||||
// Compile asset
|
// Compile asset
|
||||||
compiled, err := a.compileAsset(chromaTempFileName)
|
compiled, err := a.compileAsset(chromaPath, &cssBuffer)
|
||||||
_ = os.Remove(chromaTempFileName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
a.assetFileNames["css/chroma.css"] = compiled
|
// Add to map
|
||||||
|
a.assetFileNames[chromaPath] = compiled
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
58
tts.go
58
tts.go
|
@ -1,14 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"strings"
|
||||||
|
|
||||||
"github.com/carlmjohnson/requests"
|
"github.com/carlmjohnson/requests"
|
||||||
)
|
)
|
||||||
|
@ -54,32 +56,29 @@ func (a *goBlog) createPostTTSAudio(p *post) error {
|
||||||
if lang == "" {
|
if lang == "" {
|
||||||
lang = "en"
|
lang = "en"
|
||||||
}
|
}
|
||||||
text := a.renderMdTitle(p.Title()) + "\n\n" + cleanHTMLText(string(a.postHtml(p, false)))
|
|
||||||
|
|
||||||
// Generate audio file
|
// Build SSML
|
||||||
tmpDir, err := os.MkdirTemp("", "")
|
var ssml strings.Builder
|
||||||
if err != nil {
|
ssml.WriteString("<speak>")
|
||||||
return err
|
ssml.WriteString(html.EscapeString(a.renderMdTitle(p.Title())))
|
||||||
}
|
ssml.WriteString("<break time=\"1s\"/>")
|
||||||
defer func() {
|
ssml.WriteString(html.EscapeString(cleanHTMLText(string(a.postHtml(p, false)))))
|
||||||
_ = os.RemoveAll(tmpDir)
|
ssml.WriteString("</speak>")
|
||||||
}()
|
|
||||||
outputFileName := filepath.Join(tmpDir, "audio.mp3")
|
// Generate audio
|
||||||
err = a.createTTSAudio(lang, text, outputFileName)
|
var audioBuffer bytes.Buffer
|
||||||
|
err := a.createTTSAudio(lang, ssml.String(), &audioBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new audio file
|
// Save audio
|
||||||
file, err := os.Open(outputFileName)
|
audioReader := bytes.NewReader(audioBuffer.Bytes())
|
||||||
|
fileHash, err := getSHA256(audioReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fileHash, err := getSHA256(file)
|
loc, err := a.saveMediaFile(fileHash+".mp3", audioReader)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
loc, err := a.saveMediaFile(fileHash+".mp3", file)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -133,7 +132,7 @@ func (a *goBlog) deletePostTTSAudio(p *post) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *goBlog) createTTSAudio(lang, text, outputFile string) error {
|
func (a *goBlog) createTTSAudio(lang, ssml string, w io.Writer) error {
|
||||||
// Check if Google Cloud TTS is enabled
|
// Check if Google Cloud TTS is enabled
|
||||||
gctts := a.cfg.TTS
|
gctts := a.cfg.TTS
|
||||||
if !gctts.Enabled || gctts.GoogleAPIKey == "" {
|
if !gctts.Enabled || gctts.GoogleAPIKey == "" {
|
||||||
|
@ -144,20 +143,26 @@ func (a *goBlog) createTTSAudio(lang, text, outputFile string) error {
|
||||||
if lang == "" {
|
if lang == "" {
|
||||||
return errors.New("language not provided")
|
return errors.New("language not provided")
|
||||||
}
|
}
|
||||||
if text == "" {
|
if ssml == "" {
|
||||||
return errors.New("empty text")
|
return errors.New("empty text")
|
||||||
}
|
}
|
||||||
if outputFile == "" {
|
if w == nil {
|
||||||
return errors.New("output file not provided")
|
return errors.New("writer not provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check max length
|
||||||
|
// TODO: Support longer texts by splitting into multiple requests
|
||||||
|
// if len(ssml) > 5000 {
|
||||||
|
// return errors.New("text is too long")
|
||||||
|
// }
|
||||||
|
|
||||||
// Create request body
|
// Create request body
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"audioConfig": map[string]interface{}{
|
"audioConfig": map[string]interface{}{
|
||||||
"audioEncoding": "MP3",
|
"audioEncoding": "MP3",
|
||||||
},
|
},
|
||||||
"input": map[string]interface{}{
|
"input": map[string]interface{}{
|
||||||
"text": text,
|
"ssml": ssml,
|
||||||
},
|
},
|
||||||
"voice": map[string]interface{}{
|
"voice": map[string]interface{}{
|
||||||
"languageCode": lang,
|
"languageCode": lang,
|
||||||
|
@ -183,7 +188,8 @@ func (a *goBlog) createTTSAudio(lang, text, outputFile string) error {
|
||||||
if encoded, ok := response["audioContent"]; ok {
|
if encoded, ok := response["audioContent"]; ok {
|
||||||
if encodedStr, ok := encoded.(string); ok {
|
if encodedStr, ok := encoded.(string); ok {
|
||||||
if audio, err := base64.StdEncoding.DecodeString(encodedStr); err == nil {
|
if audio, err := base64.StdEncoding.DecodeString(encodedStr); err == nil {
|
||||||
return os.WriteFile(outputFile, audio, os.ModePerm)
|
_, err := w.Write(audio)
|
||||||
|
return err
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
20
utils.go
20
utils.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -263,8 +264,7 @@ func htmlText(s string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanHTMLText(s string) string {
|
func cleanHTMLText(s string) string {
|
||||||
s = bluemonday.UGCPolicy().Sanitize(s)
|
return htmlText(bluemonday.UGCPolicy().Sanitize(s))
|
||||||
return htmlText(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultIfEmpty(s, d string) string {
|
func defaultIfEmpty(s, d string) string {
|
||||||
|
@ -327,3 +327,19 @@ func saveToFile(reader io.Reader, fileName string) error {
|
||||||
_, err = io.Copy(out, reader)
|
_, err = io.Copy(out, reader)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type valueOnlyContext struct {
|
||||||
|
context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (valueOnlyContext) Deadline() (deadline time.Time, ok bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (valueOnlyContext) Done() <-chan struct{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (valueOnlyContext) Err() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue