Compare commits

...

6 Commits

  1. 1
      flake.nix
  2. 49
      src/cache/cache.go
  3. 8
      src/cmd/load-test-data/test-data.yml
  4. 5
      src/cmd/mediocre-blog/main.go
  5. 87
      src/gmi/cache.go
  6. 27
      src/gmi/gemtext.go
  7. 17
      src/gmi/gemtext_test.go
  8. 31
      src/gmi/gmi.go
  9. 28
      src/gmi/tpl.go
  10. 7
      src/gmi/tpl/index.gmi
  11. 2
      src/gmi/tpl/posts/index.gmi
  12. 10
      src/gmi/tpl/posts/post.gmi
  13. 3
      src/http/apiutil/apiutil.go
  14. 25
      src/http/http.go
  15. 23
      src/http/middleware.go
  16. 9
      src/http/posts.go
  17. 4
      src/http/tpl/draft-posts-manage.html
  18. 2
      src/http/tpl/index.html
  19. 4
      src/http/tpl/posts-manage.html
  20. 17
      src/post/preprocess.go

@ -43,6 +43,7 @@
export MEDIOCRE_BLOG_HTTP_PUBLIC_URL="$MEDIOCRE_BLOG_ML_PUBLIC_URL" export MEDIOCRE_BLOG_HTTP_PUBLIC_URL="$MEDIOCRE_BLOG_ML_PUBLIC_URL"
export MEDIOCRE_BLOG_HTTP_LISTEN_PROTO="tcp" export MEDIOCRE_BLOG_HTTP_LISTEN_PROTO="tcp"
export MEDIOCRE_BLOG_HTTP_LISTEN_ADDR=":4000" export MEDIOCRE_BLOG_HTTP_LISTEN_ADDR=":4000"
export MEDIOCRE_BLOG_HTTP_GEMINI_GATEWAY_URL="https://nightfall.city/x/"
# http auth # http auth
# (password is "bar". This should definitely be changed for prod.) # (password is "bar". This should definitely be changed for prod.)

49
src/cache/cache.go vendored

@ -0,0 +1,49 @@
// Package cache implements a simple LRU cache which can be completely purged
// whenever needed.
package cache
import (
lru "github.com/hashicorp/golang-lru"
)
// Cache describes an in-memory cache for arbitrary objects, which has a Purge
// method for clearing everything at once.
type Cache interface {
Get(key string) interface{}
Set(key string, value interface{})
Purge()
}
type cache struct {
cache *lru.Cache
}
// New instantiates and returns a new Cache which can hold up to the given
// number of entries.
func New(size int) Cache {
c, err := lru.New(size)
// instantiating the lru cache can't realistically fail
if err != nil {
panic(err)
}
return &cache{c}
}
func (c *cache) Get(key string) interface{} {
value, ok := c.cache.Get(key)
if !ok {
return nil
}
return value
}
func (c *cache) Set(key string, value interface{}) {
c.cache.Add(key, value)
}
func (c *cache) Purge() {
c.cache.Purge()
}

@ -104,6 +104,12 @@ published_posts:
Edgy. Edgy.
=> / Here's a link within the site
=> gemini://mediocregopher.com And here's a link to a gemini capsule
=> https://mediocregopher.com And here's a link to an https site
#### Side-note #### Side-note
Did you know that the terms "cyberspace" and "matrix" are attributable to a book from 1984 called _Neuromancer_? Did you know that the terms "cyberspace" and "matrix" are attributable to a book from 1984 called _Neuromancer_?
@ -117,8 +123,6 @@ published_posts:
This has been a great post. This has been a great post.
=> / Here's a link outa here!
- id: empty-markdown-test - id: empty-markdown-test
title: Empty Markdown Test title: Empty Markdown Test
description: description:

@ -7,6 +7,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/gmi" "github.com/mediocregopher/blog.mediocregopher.com/srv/gmi"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http" "github.com/mediocregopher/blog.mediocregopher.com/srv/http"
@ -102,7 +103,10 @@ func main() {
postAssetStore := post.NewAssetStore(postSQLDB) postAssetStore := post.NewAssetStore(postSQLDB)
postDraftStore := post.NewDraftStore(postSQLDB) postDraftStore := post.NewDraftStore(postSQLDB)
cache := cache.New(5000)
httpParams.Logger = logger.WithNamespace("http") httpParams.Logger = logger.WithNamespace("http")
httpParams.Cache = cache
httpParams.PowManager = powMgr httpParams.PowManager = powMgr
httpParams.PostStore = postStore httpParams.PostStore = postStore
httpParams.PostAssetStore = postAssetStore httpParams.PostAssetStore = postAssetStore
@ -124,6 +128,7 @@ func main() {
}() }()
gmiParams.Logger = logger.WithNamespace("gmi") gmiParams.Logger = logger.WithNamespace("gmi")
gmiParams.Cache = cache
gmiParams.PostStore = postStore gmiParams.PostStore = postStore
gmiParams.PostAssetStore = postAssetStore gmiParams.PostAssetStore = postAssetStore
gmiParams.HTTPPublicURL = httpParams.PublicURL gmiParams.HTTPPublicURL = httpParams.PublicURL

@ -0,0 +1,87 @@
package gmi
import (
"bytes"
"context"
"io"
"sync"
"git.sr.ht/~adnano/go-gemini"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
)
type cacheRW struct {
gemini.ResponseWriter
status gemini.Status
mediaType string
buf []byte
}
func (c *cacheRW) SetMediaType(mediaType string) {
c.mediaType = mediaType
c.ResponseWriter.SetMediaType(mediaType)
}
func (c *cacheRW) WriteHeader(status gemini.Status, meta string) {
c.status = status
c.ResponseWriter.WriteHeader(status, meta)
}
func (c *cacheRW) Write(b []byte) (int, error) {
c.buf = append(c.buf, b...)
return c.ResponseWriter.Write(b)
}
func cacheMiddleware(cache cache.Cache) func(h gemini.Handler) gemini.Handler {
type entry struct {
mediaType string
body []byte
}
pool := sync.Pool{
New: func() interface{} { return new(bytes.Reader) },
}
return func(h gemini.Handler) gemini.Handler {
return gemini.HandlerFunc(func(
ctx context.Context,
rw gemini.ResponseWriter,
r *gemini.Request,
) {
id := r.URL.String()
if value := cache.Get(id); value != nil {
entry := value.(entry)
if entry.mediaType != "" {
rw.SetMediaType(entry.mediaType)
}
reader := pool.Get().(*bytes.Reader)
defer pool.Put(reader)
reader.Reset(entry.body)
io.Copy(rw, reader)
return
}
cacheRW := &cacheRW{
ResponseWriter: rw,
status: gemini.StatusSuccess,
}
h.ServeGemini(ctx, cacheRW, r)
if cacheRW.status == gemini.StatusSuccess {
cache.Set(id, entry{
mediaType: cacheRW.mediaType,
body: cacheRW.buf,
})
}
})
}
}

@ -5,8 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log" "net/url"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
) )
@ -25,7 +26,10 @@ var linkRegexp = regexp.MustCompile(`^=>\s+(\S+)\s*(.*?)\s*$`)
// GemtextToMarkdown reads a gemtext formatted body from the Reader and writes // GemtextToMarkdown reads a gemtext formatted body from the Reader and writes
// the markdown version of that body to the Writer. // the markdown version of that body to the Writer.
func GemtextToMarkdown(dst io.Writer, src io.Reader) error { //
// gmiGateway, if given, is used for all `gemini://` links. The `gemini://`
// prefix will be stripped, and replaced with the given URL.
func GemtextToMarkdown(dst io.Writer, src io.Reader, gmiGateway *url.URL) error {
bufSrc := bufio.NewReader(src) bufSrc := bufio.NewReader(src)
@ -40,7 +44,20 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
if match := linkRegexp.FindStringSubmatch(line); len(match) > 0 { if match := linkRegexp.FindStringSubmatch(line); len(match) > 0 {
isImg := hasImgExt(match[1]) u, err := url.Parse(match[1])
if err != nil {
return fmt.Errorf("link to invalid url %q: %w", match[1], err)
}
if u.Scheme == "gemini" && gmiGateway != nil {
newU := *gmiGateway
newU.Path = filepath.Join(newU.Path, u.Host, u.Path)
newU.RawQuery = u.RawQuery
u = &newU
}
isImg := hasImgExt(u.Path)
descr := match[2] descr := match[2]
@ -52,9 +69,7 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
descr = "Link" descr = "Link"
} }
log.Printf("descr:%q", descr) line = fmt.Sprintf("[%s](%s)\n", descr, u.String())
line = fmt.Sprintf("[%s](%s)\n", descr, match[1])
if isImg { if isImg {
line = "!" + line line = "!" + line

@ -2,6 +2,7 @@ package gmi
import ( import (
"bytes" "bytes"
"net/url"
"strconv" "strconv"
"testing" "testing"
@ -10,6 +11,8 @@ import (
func TestGemtextToMarkdown(t *testing.T) { func TestGemtextToMarkdown(t *testing.T) {
gmiGateway, _ := url.Parse("https://gateway.com/x/")
tests := []struct { tests := []struct {
in, exp string in, exp string
}{ }{
@ -37,13 +40,25 @@ func TestGemtextToMarkdown(t *testing.T) {
in: "=> img.png description is here ", in: "=> img.png description is here ",
exp: "![description is here](img.png)\n", exp: "![description is here](img.png)\n",
}, },
{
in: "=> gemini://somewhere.com/foo Somewhere",
exp: "[Somewhere](https://gateway.com/x/somewhere.com/foo)\n",
},
{
in: "=> gemini://somewhere.com:420/foo Somewhere",
exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo)\n",
},
{
in: "=> gemini://somewhere.com:420/foo?bar=baz Somewhere",
exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo?bar=baz)\n",
},
} }
for i, test := range tests { for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) { t.Run(strconv.Itoa(i), func(t *testing.T) {
got := new(bytes.Buffer) got := new(bytes.Buffer)
err := GemtextToMarkdown(got, bytes.NewBufferString(test.in)) err := GemtextToMarkdown(got, bytes.NewBufferString(test.in), gmiGateway)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, test.exp, got.String()) assert.Equal(t, test.exp, got.String())
}) })

@ -13,6 +13,7 @@ import (
"git.sr.ht/~adnano/go-gemini" "git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate" "git.sr.ht/~adnano/go-gemini/certificate"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/post" "github.com/mediocregopher/blog.mediocregopher.com/srv/post"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/mediocregopher/mediocre-go-lib/v2/mctx"
@ -23,6 +24,7 @@ import (
// unless otherwise noted. // unless otherwise noted.
type Params struct { type Params struct {
Logger *mlog.Logger Logger *mlog.Logger
Cache cache.Cache
PostStore post.Store PostStore post.Store
PostAssetStore post.AssetStore PostAssetStore post.AssetStore
@ -108,10 +110,11 @@ func New(params Params) (API, error) {
go func() { go func() {
ctx := mctx.WithAnnotator(context.Background(), &a.params) err := a.srv.ListenAndServe(context.Background())
err := a.srv.ListenAndServe(ctx)
if err != nil && !errors.Is(err, context.Canceled) { if err != nil && !errors.Is(err, context.Canceled) {
ctx := mctx.WithAnnotator(context.Background(), &a.params)
a.params.Logger.Fatal(ctx, "serving gemini server", err) a.params.Logger.Fatal(ctx, "serving gemini server", err)
} }
}() }()
@ -123,6 +126,26 @@ func (a *api) Shutdown(ctx context.Context) error {
return a.srv.Shutdown(ctx) return a.srv.Shutdown(ctx)
} }
func (a *api) logReqMiddleware(h gemini.Handler) gemini.Handler {
type logCtxKey string
return gemini.HandlerFunc(func(
ctx context.Context,
rw gemini.ResponseWriter,
r *gemini.Request,
) {
ctx = mctx.Annotate(ctx,
logCtxKey("url"), r.URL.String(),
)
h.ServeGemini(ctx, rw, r)
a.params.Logger.Info(ctx, "handled gemini request")
})
}
func indexMiddleware(h gemini.Handler) gemini.Handler { func indexMiddleware(h gemini.Handler) gemini.Handler {
return gemini.HandlerFunc(func( return gemini.HandlerFunc(func(
@ -212,8 +235,8 @@ func (a *api) handler() (gemini.Handler, error) {
h = mux h = mux
h = indexMiddleware(h) h = indexMiddleware(h)
// TODO logging h = a.logReqMiddleware(h)
// TODO caching h = cacheMiddleware(a.params.Cache)(h)
return h, nil return h, nil
} }

@ -134,11 +134,12 @@ func (r renderer) Add(a, b int) int { return a + b }
func (a *api) tplHandler() (gemini.Handler, error) { func (a *api) tplHandler() (gemini.Handler, error) {
blogURL := func(path string, abs bool) string { blogURL := func(base *url.URL, path string, abs bool) string {
// filepath.Join strips trailing slash, but we want to keep it // filepath.Join strips trailing slash, but we want to keep it
trailingSlash := strings.HasSuffix(path, "/") trailingSlash := strings.HasSuffix(path, "/")
path = filepath.Join("/", a.params.PublicURL.Path, path) path = filepath.Join("/", base.Path, path)
if trailingSlash && path != "/" { if trailingSlash && path != "/" {
path += "/" path += "/"
@ -148,27 +149,29 @@ func (a *api) tplHandler() (gemini.Handler, error) {
return path return path
} }
u := *a.params.PublicURL u := *base
u.Path = path u.Path = path
return u.String() return u.String()
} }
preprocessFuncs := post.PreprocessFunctions{ preprocessFuncs := post.PreprocessFunctions{
BlogURL: func(path string) string { BlogURL: func(path string) string {
return blogURL(path, false) return blogURL(a.params.PublicURL, path, false)
},
BlogHTTPURL: func(path string) string {
return blogURL(a.params.HTTPPublicURL, path, true)
}, },
AssetURL: func(id string) string { AssetURL: func(id string) string {
path := filepath.Join("assets", id) path := filepath.Join("assets", id)
return blogURL(path, false) return blogURL(a.params.PublicURL, path, false)
}, },
PostURL: func(id string) string { PostURL: func(id string) string {
path := filepath.Join("posts", id) + ".gmi" path := filepath.Join("posts", id) + ".gmi"
return blogURL(path, false) return blogURL(a.params.PublicURL, path, false)
}, },
StaticURL: func(path string) string { StaticURL: func(path string) string {
httpPublicURL := *a.params.HTTPPublicURL path = filepath.Join("static", path)
httpPublicURL.Path = filepath.Join(httpPublicURL.Path, "/static", path) return blogURL(a.params.HTTPPublicURL, path, true)
return httpPublicURL.String()
}, },
Image: func(args ...string) (string, error) { Image: func(args ...string) (string, error) {
@ -181,7 +184,8 @@ func (a *api) tplHandler() (gemini.Handler, error) {
descr = args[1] descr = args[1]
} }
path := blogURL(filepath.Join("assets", id), false) path := filepath.Join("assets", id)
path = blogURL(a.params.PublicURL, path, false)
return fmt.Sprintf("\n=> %s %s", path, descr), nil return fmt.Sprintf("\n=> %s %s", path, descr), nil
}, },
@ -193,11 +197,11 @@ func (a *api) tplHandler() (gemini.Handler, error) {
allTpls.Funcs(template.FuncMap{ allTpls.Funcs(template.FuncMap{
"BlogURLAbs": func(path string) string { "BlogURLAbs": func(path string) string {
return blogURL(path, true) return blogURL(a.params.PublicURL, path, true)
}, },
"PostURLAbs": func(id string) string { "PostURLAbs": func(id string) string {
path := filepath.Join("posts", id) + ".gmi" path := filepath.Join("posts", id) + ".gmi"
return blogURL(path, true) return blogURL(a.params.PublicURL, path, true)
}, },
}) })

@ -1,7 +1,6 @@
# mediocregopher's lil web corner # mediocregopher's lil web corner
This here's my little corner of the web, where I publish posts about projects This here's my little corner of the web, where I publish posts about projects I'm working on and things that interest me (which you can follow, if you like).
I'm working on and things that interest me (which you can follow, if you like).
=> {{ BlogURL "posts/" }} Browse all posts => {{ BlogURL "posts/" }} Browse all posts
@ -22,7 +21,7 @@ Feel free to hmu on any of these if you'd like to get in touch.
* Mastodon: @mediocregopher@social.cryptic.io * Mastodon: @mediocregopher@social.cryptic.io
* Email: mediocregopher@gmail.com * Email: me@mediocregopher.com
## Dev ## Dev
@ -48,8 +47,6 @@ Feel free to hmu on any of these if you'd like to get in touch.
=> https://news.cryptic.io/ Cryptic News aggregates interesting blogs. => https://news.cryptic.io/ Cryptic News aggregates interesting blogs.
--------------------------------------------------------------------------------
I'm not affiliated with these, but they're worth listing. I'm not affiliated with these, but they're worth listing.
=> https://search.marginalia.nu/ Marginalia reminds me of the old internet. => https://search.marginalia.nu/ Marginalia reminds me of the old internet.

@ -23,7 +23,7 @@ much as the quality!
{{ if $getPostsRes.HasMore -}} {{ if $getPostsRes.HasMore -}}
=> {{ BlogURL "posts" }}/?page={{ .Add $page 1 }} Next page => {{ BlogURL "posts" }}/?page={{ .Add $page 1 }} Next page
{{ end }} {{ end }}
-------------------------------------------------------------------------------- ================================================================================
=> {{ BlogURL "feed.xml" }} Subscribe to the RSS feed for updates => {{ BlogURL "feed.xml" }} Subscribe to the RSS feed for updates

@ -9,15 +9,14 @@
{{ .PostBody $post }} {{ .PostBody $post }}
-------------------------------------------------------------------------------- ================================================================================
Published {{ $post.PublishedAt.Format "2006-01-02" }} Published {{ $post.PublishedAt.Format "2006-01-02" }} by mediocregopher
{{- if $post.Series }} {{- if $post.Series }}
This post is part of a series!
{{ $seriesNextPrev := .GetPostSeriesNextPrevious $post -}} {{ $seriesNextPrev := .GetPostSeriesNextPrevious $post -}}
{{ if or $seriesNextPrev.Next $seriesNextPrev.Previous }}
This post is part of a series!
{{ if $seriesNextPrev.Next -}} {{ if $seriesNextPrev.Next -}}
=> {{ BlogURL "posts" }}/{{ $seriesNextPrev.Next.ID }}.gmi Next in the series: {{ $seriesNextPrev.Next.Title }} => {{ BlogURL "posts" }}/{{ $seriesNextPrev.Next.ID }}.gmi Next in the series: {{ $seriesNextPrev.Next.Title }}
@ -27,6 +26,7 @@ This post is part of a series!
=> {{ BlogURL "posts" }}/{{ $seriesNextPrev.Previous.ID }}.gmi Prevous in the series: {{ $seriesNextPrev.Previous.Title }} => {{ BlogURL "posts" }}/{{ $seriesNextPrev.Previous.ID }}.gmi Prevous in the series: {{ $seriesNextPrev.Previous.Title }}
{{ end -}} {{ end -}}
{{ end -}}
{{ end }} {{ end }}
=> {{ BlogURL "posts/" }} Browse all posts => {{ BlogURL "posts/" }} Browse all posts

@ -16,6 +16,9 @@ import (
"github.com/mediocregopher/mediocre-go-lib/v2/mlog" "github.com/mediocregopher/mediocre-go-lib/v2/mlog"
) )
// TODO I don't think Set/GetRequestLogger are necessary? Seems sufficient to
// just annotate the request's context
type loggerCtxKey int type loggerCtxKey int
// SetRequestLogger sets the given Logger onto the given Request's Context, // SetRequestLogger sets the given Logger onto the given Request's Context,

@ -15,7 +15,7 @@ import (
"strings" "strings"
"time" "time"
lru "github.com/hashicorp/golang-lru" "github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
@ -33,6 +33,7 @@ var staticFS embed.FS
type Params struct { type Params struct {
Logger *mlog.Logger Logger *mlog.Logger
PowManager pow.Manager PowManager pow.Manager
Cache cache.Cache
PostStore post.Store PostStore post.Store
PostAssetStore post.AssetStore PostAssetStore post.AssetStore
@ -56,12 +57,17 @@ type Params struct {
// AuthRatelimit indicates how much time must pass between subsequent auth // AuthRatelimit indicates how much time must pass between subsequent auth
// attempts. // attempts.
AuthRatelimit time.Duration AuthRatelimit time.Duration
// GeminiGatewayURL will be used to translate links for `gemini://` into
// `http(s)://`. See gmi.GemtextToMarkdown.
GeminiGatewayURL *url.URL
} }
// SetupCfg implement the cfg.Cfger interface. // SetupCfg implement the cfg.Cfger interface.
func (p *Params) SetupCfg(cfg *cfg.Cfg) { func (p *Params) SetupCfg(cfg *cfg.Cfg) {
publicURLStr := cfg.String("http-public-url", "http://localhost:4000", "URL this service is accessible at") publicURLStr := cfg.String("http-public-url", "http://localhost:4000", "URL this service is accessible at")
geminiGatewayURLStr := cfg.String("http-gemini-gateway-url", "", "Optional URL to prefix to all gemini:// links, to make them accessible over https")
cfg.StringVar(&p.ListenProto, "http-listen-proto", "tcp", "Protocol to listen for HTTP requests with") cfg.StringVar(&p.ListenProto, "http-listen-proto", "tcp", "Protocol to listen for HTTP requests with")
cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/unix socket path to listen for HTTP requests on") cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/unix socket path to listen for HTTP requests on")
@ -87,6 +93,12 @@ func (p *Params) SetupCfg(cfg *cfg.Cfg) {
return fmt.Errorf("parsing -http-public-url: %w", err) return fmt.Errorf("parsing -http-public-url: %w", err)
} }
if *geminiGatewayURLStr != "" {
if p.GeminiGatewayURL, err = url.Parse(*geminiGatewayURLStr); err != nil {
return fmt.Errorf("parsing -http-gemini-gateway-url: %w", err)
}
}
return nil return nil
}) })
} }
@ -179,13 +191,6 @@ func (a *api) apiHandler() http.Handler {
func (a *api) blogHandler() http.Handler { func (a *api) blogHandler() http.Handler {
cache, err := lru.New(5000)
// instantiating the lru cache can't realistically fail
if err != nil {
panic(err)
}
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/posts/", http.StripPrefix("/posts", mux.Handle("/posts/", http.StripPrefix("/posts",
@ -233,11 +238,11 @@ func (a *api) blogHandler() http.Handler {
readOnlyMiddlewares := []middleware{ readOnlyMiddlewares := []middleware{
logReqMiddleware, // only log GETs on cache miss logReqMiddleware, // only log GETs on cache miss
cacheMiddleware(cache), cacheMiddleware(a.params.Cache, a.params.PublicURL),
} }
readWriteMiddlewares := []middleware{ readWriteMiddlewares := []middleware{
purgeCacheOnOKMiddleware(cache), purgeCacheOnOKMiddleware(a.params.Cache),
authMiddleware(a.auther), authMiddleware(a.auther),
} }

@ -4,11 +4,12 @@ import (
"bytes" "bytes"
"net" "net"
"net/http" "net/http"
"net/url"
"path/filepath" "path/filepath"
"sync" "sync"
"time" "time"
lru "github.com/hashicorp/golang-lru" "github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx" "github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog" "github.com/mediocregopher/mediocre-go-lib/v2/mlog"
@ -86,6 +87,9 @@ func (rw *wrappedResponseWriter) WriteHeader(statusCode int) {
} }
func logReqMiddleware(h http.Handler) http.Handler { func logReqMiddleware(h http.Handler) http.Handler {
type logCtxKey string
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
wrw := newWrappedResponseWriter(rw) wrw := newWrappedResponseWriter(rw)
@ -94,8 +98,6 @@ func logReqMiddleware(h http.Handler) http.Handler {
h.ServeHTTP(wrw, r) h.ServeHTTP(wrw, r)
took := time.Since(started) took := time.Since(started)
type logCtxKey string
ctx := r.Context() ctx := r.Context()
ctx = mctx.Annotate(ctx, ctx = mctx.Annotate(ctx,
logCtxKey("took"), took.String(), logCtxKey("took"), took.String(),
@ -139,7 +141,7 @@ func (rw *cacheResponseWriter) Write(b []byte) (int, error) {
return rw.wrappedResponseWriter.Write(b) return rw.wrappedResponseWriter.Write(b)
} }
func cacheMiddleware(cache *lru.Cache) middleware { func cacheMiddleware(cache cache.Cache, publicURL *url.URL) middleware {
type entry struct { type entry struct {
body []byte body []byte
@ -153,11 +155,14 @@ func cacheMiddleware(cache *lru.Cache) middleware {
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
id := r.URL.RequestURI() // r.URL doesn't have Scheme or Host populated, better to add the
// public url to the key to make sure there's no possiblity of
// collision with other protocols using the cache.
id := publicURL.String() + "|" + r.URL.String()
if val, ok := cache.Get(id); ok { if value := cache.Get(id); value != nil {
entry := val.(entry) entry := value.(entry)
reader := pool.Get().(*bytes.Reader) reader := pool.Get().(*bytes.Reader)
defer pool.Put(reader) defer pool.Put(reader)
@ -174,7 +179,7 @@ func cacheMiddleware(cache *lru.Cache) middleware {
h.ServeHTTP(cacheRW, r) h.ServeHTTP(cacheRW, r)
if cacheRW.statusCode == 200 { if cacheRW.statusCode == 200 {
cache.Add(id, entry{ cache.Set(id, entry{
body: cacheRW.buf.Bytes(), body: cacheRW.buf.Bytes(),
createdAt: time.Now(), createdAt: time.Now(),
}) })
@ -183,7 +188,7 @@ func cacheMiddleware(cache *lru.Cache) middleware {
} }
} }
func purgeCacheOnOKMiddleware(cache *lru.Cache) middleware { func purgeCacheOnOKMiddleware(cache cache.Cache) middleware {
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

@ -72,6 +72,9 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload,
BlogURL: func(path string) string { BlogURL: func(path string) string {
return a.blogURL(path, false) return a.blogURL(path, false)
}, },
BlogHTTPURL: func(path string) string {
return a.blogURL(path, false)
},
AssetURL: func(id string) string { AssetURL: func(id string) string {
return a.assetURL(id, false) return a.assetURL(id, false)
}, },
@ -96,7 +99,11 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload,
prevBodyBuf := bodyBuf prevBodyBuf := bodyBuf
bodyBuf = new(bytes.Buffer) bodyBuf = new(bytes.Buffer)
if err := gmi.GemtextToMarkdown(bodyBuf, prevBodyBuf); err != nil { err := gmi.GemtextToMarkdown(
bodyBuf, prevBodyBuf, a.params.GeminiGatewayURL,
)
if err != nil {
return postTplPayload{}, fmt.Errorf("converting gemtext to markdown: %w", err) return postTplPayload{}, fmt.Errorf("converting gemtext to markdown: %w", err)
} }
} }

@ -12,7 +12,7 @@
{{ if ge .Payload.PrevPage 0 }} {{ if ge .Payload.PrevPage 0 }}
<p> <p>
<a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a> <a href="?method=manage&p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
</p> </p>
{{ end }} {{ end }}
@ -41,7 +41,7 @@
{{ if ge .Payload.NextPage 0 }} {{ if ge .Payload.NextPage 0 }}
<p> <p>
<a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a> <a href="?method=manage&p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
</p> </p>
{{ end }} {{ end }}

@ -13,7 +13,7 @@
<ul> <ul>
<li>Matrix: <a href="https://matrix.to/#/@mediocregopher:waffle.farm">@mediocregopher:waffle.farm</a></li> <li>Matrix: <a href="https://matrix.to/#/@mediocregopher:waffle.farm">@mediocregopher:waffle.farm</a></li>
<li>Mastodon: <a href="https://social.cryptic.io/@mediocregopher">@mediocregopher@social.cryptic.io</a></li> <li>Mastodon: <a href="https://social.cryptic.io/@mediocregopher">@mediocregopher@social.cryptic.io</a></li>
<li>Email: <a href="mailto:mediocregopher@gmail.com">mediocregopher@gmail.com</a></li> <li>Email: <a href="mailto:me@mediocregopher.com">me@mediocregopher.com</a></li>
</ul> </ul>
<h2>Dev</h2> <h2>Dev</h2>

@ -8,7 +8,7 @@
{{ if ge .Payload.PrevPage 0 }} {{ if ge .Payload.PrevPage 0 }}
<p> <p>
<a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a> <a href="?method=manage&p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
</p> </p>
{{ end }} {{ end }}
@ -38,7 +38,7 @@
{{ if ge .Payload.NextPage 0 }} {{ if ge .Payload.NextPage 0 }}
<p> <p>
<a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a> <a href="?method=manage&p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
</p> </p>
{{ end }} {{ end }}

@ -17,6 +17,12 @@ type PreprocessFunctions struct {
// The given path should not have a leading slash. // The given path should not have a leading slash.
BlogURL func(path string) string BlogURL func(path string) string
// BlogURL returns the given string, rooted to the base URL of the blog's
// HTTP server (which may or may not include path components itself).
//
// The given path should not have a leading slash.
BlogHTTPURL func(path string) string
// AssetURL returns the URL of the asset with the given ID. // AssetURL returns the URL of the asset with the given ID.
AssetURL func(id string) string AssetURL func(id string) string
@ -39,11 +45,12 @@ type PreprocessFunctions struct {
func (funcs PreprocessFunctions) ToFuncsMap() template.FuncMap { func (funcs PreprocessFunctions) ToFuncsMap() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
"BlogURL": funcs.BlogURL, "BlogURL": funcs.BlogURL,
"AssetURL": funcs.AssetURL, "BlogHTTPURL": funcs.BlogHTTPURL,
"PostURL": funcs.PostURL, "AssetURL": funcs.AssetURL,
"StaticURL": funcs.StaticURL, "PostURL": funcs.PostURL,
"Image": funcs.Image, "StaticURL": funcs.StaticURL,
"Image": funcs.Image,
} }
} }

Loading…
Cancel
Save