Compare commits
6 Commits
acec797048
...
c1c1bb2c4c
Author | SHA1 | Date | |
---|---|---|---|
|
c1c1bb2c4c | ||
|
024f514886 | ||
|
26dbc6691d | ||
|
57719f29b9 | ||
|
c4520f2c84 | ||
|
aba69d4329 |
@ -43,6 +43,7 @@
|
||||
export MEDIOCRE_BLOG_HTTP_PUBLIC_URL="$MEDIOCRE_BLOG_ML_PUBLIC_URL"
|
||||
export MEDIOCRE_BLOG_HTTP_LISTEN_PROTO="tcp"
|
||||
export MEDIOCRE_BLOG_HTTP_LISTEN_ADDR=":4000"
|
||||
export MEDIOCRE_BLOG_HTTP_GEMINI_GATEWAY_URL="https://nightfall.city/x/"
|
||||
|
||||
# http auth
|
||||
# (password is "bar". This should definitely be changed for prod.)
|
||||
|
49
src/cache/cache.go
vendored
Normal file
49
src/cache/cache.go
vendored
Normal file
@ -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.
|
||||
|
||||
=> / 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
|
||||
|
||||
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.
|
||||
|
||||
=> / Here's a link outa here!
|
||||
|
||||
- id: empty-markdown-test
|
||||
title: Empty Markdown Test
|
||||
description:
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
|
||||
cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
|
||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/gmi"
|
||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/http"
|
||||
@ -102,7 +103,10 @@ func main() {
|
||||
postAssetStore := post.NewAssetStore(postSQLDB)
|
||||
postDraftStore := post.NewDraftStore(postSQLDB)
|
||||
|
||||
cache := cache.New(5000)
|
||||
|
||||
httpParams.Logger = logger.WithNamespace("http")
|
||||
httpParams.Cache = cache
|
||||
httpParams.PowManager = powMgr
|
||||
httpParams.PostStore = postStore
|
||||
httpParams.PostAssetStore = postAssetStore
|
||||
@ -124,6 +128,7 @@ func main() {
|
||||
}()
|
||||
|
||||
gmiParams.Logger = logger.WithNamespace("gmi")
|
||||
gmiParams.Cache = cache
|
||||
gmiParams.PostStore = postStore
|
||||
gmiParams.PostAssetStore = postAssetStore
|
||||
gmiParams.HTTPPublicURL = httpParams.PublicURL
|
||||
|
87
src/gmi/cache.go
Normal file
87
src/gmi/cache.go
Normal file
@ -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"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@ -25,7 +26,10 @@ var linkRegexp = regexp.MustCompile(`^=>\s+(\S+)\s*(.*?)\s*$`)
|
||||
|
||||
// GemtextToMarkdown reads a gemtext formatted body from the Reader and writes
|
||||
// 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)
|
||||
|
||||
@ -40,7 +44,20 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
|
||||
|
||||
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]
|
||||
|
||||
@ -52,9 +69,7 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
|
||||
descr = "Link"
|
||||
}
|
||||
|
||||
log.Printf("descr:%q", descr)
|
||||
|
||||
line = fmt.Sprintf("[%s](%s)\n", descr, match[1])
|
||||
line = fmt.Sprintf("[%s](%s)\n", descr, u.String())
|
||||
|
||||
if isImg {
|
||||
line = "!" + line
|
||||
|
@ -2,6 +2,7 @@ package gmi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
@ -10,6 +11,8 @@ import (
|
||||
|
||||
func TestGemtextToMarkdown(t *testing.T) {
|
||||
|
||||
gmiGateway, _ := url.Parse("https://gateway.com/x/")
|
||||
|
||||
tests := []struct {
|
||||
in, exp string
|
||||
}{
|
||||
@ -37,13 +40,25 @@ func TestGemtextToMarkdown(t *testing.T) {
|
||||
in: "=> img.png description is here ",
|
||||
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 {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
|
||||
got := new(bytes.Buffer)
|
||||
err := GemtextToMarkdown(got, bytes.NewBufferString(test.in))
|
||||
err := GemtextToMarkdown(got, bytes.NewBufferString(test.in), gmiGateway)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.exp, got.String())
|
||||
})
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
|
||||
"git.sr.ht/~adnano/go-gemini"
|
||||
"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/post"
|
||||
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
||||
@ -23,6 +24,7 @@ import (
|
||||
// unless otherwise noted.
|
||||
type Params struct {
|
||||
Logger *mlog.Logger
|
||||
Cache cache.Cache
|
||||
|
||||
PostStore post.Store
|
||||
PostAssetStore post.AssetStore
|
||||
@ -108,10 +110,11 @@ func New(params Params) (API, error) {
|
||||
|
||||
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) {
|
||||
|
||||
ctx := mctx.WithAnnotator(context.Background(), &a.params)
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
return gemini.HandlerFunc(func(
|
||||
@ -212,8 +235,8 @@ func (a *api) handler() (gemini.Handler, error) {
|
||||
h = mux
|
||||
h = indexMiddleware(h)
|
||||
|
||||
// TODO logging
|
||||
// TODO caching
|
||||
h = a.logReqMiddleware(h)
|
||||
h = cacheMiddleware(a.params.Cache)(h)
|
||||
|
||||
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) {
|
||||
|
||||
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
|
||||
trailingSlash := strings.HasSuffix(path, "/")
|
||||
|
||||
path = filepath.Join("/", a.params.PublicURL.Path, path)
|
||||
path = filepath.Join("/", base.Path, path)
|
||||
|
||||
if trailingSlash && path != "/" {
|
||||
path += "/"
|
||||
@ -148,27 +149,29 @@ func (a *api) tplHandler() (gemini.Handler, error) {
|
||||
return path
|
||||
}
|
||||
|
||||
u := *a.params.PublicURL
|
||||
u := *base
|
||||
u.Path = path
|
||||
return u.String()
|
||||
}
|
||||
|
||||
preprocessFuncs := post.PreprocessFunctions{
|
||||
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 {
|
||||
path := filepath.Join("assets", id)
|
||||
return blogURL(path, false)
|
||||
return blogURL(a.params.PublicURL, path, false)
|
||||
},
|
||||
PostURL: func(id string) string {
|
||||
path := filepath.Join("posts", id) + ".gmi"
|
||||
return blogURL(path, false)
|
||||
return blogURL(a.params.PublicURL, path, false)
|
||||
},
|
||||
StaticURL: func(path string) string {
|
||||
httpPublicURL := *a.params.HTTPPublicURL
|
||||
httpPublicURL.Path = filepath.Join(httpPublicURL.Path, "/static", path)
|
||||
return httpPublicURL.String()
|
||||
path = filepath.Join("static", path)
|
||||
return blogURL(a.params.HTTPPublicURL, path, true)
|
||||
},
|
||||
Image: func(args ...string) (string, error) {
|
||||
|
||||
@ -181,7 +184,8 @@ func (a *api) tplHandler() (gemini.Handler, error) {
|
||||
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
|
||||
},
|
||||
@ -193,11 +197,11 @@ func (a *api) tplHandler() (gemini.Handler, error) {
|
||||
|
||||
allTpls.Funcs(template.FuncMap{
|
||||
"BlogURLAbs": func(path string) string {
|
||||
return blogURL(path, true)
|
||||
return blogURL(a.params.PublicURL, path, true)
|
||||
},
|
||||
"PostURLAbs": func(id string) string {
|
||||
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
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
=> {{ 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
|
||||
|
||||
* Email: mediocregopher@gmail.com
|
||||
* Email: me@mediocregopher.com
|
||||
|
||||
## 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.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
I'm not affiliated with these, but they're worth listing.
|
||||
|
||||
=> https://search.marginalia.nu/ Marginalia reminds me of the old internet.
|
||||
|
@ -23,7 +23,7 @@ much as the quality!
|
||||
{{ if $getPostsRes.HasMore -}}
|
||||
=> {{ BlogURL "posts" }}/?page={{ .Add $page 1 }} Next page
|
||||
{{ end }}
|
||||
--------------------------------------------------------------------------------
|
||||
================================================================================
|
||||
|
||||
=> {{ BlogURL "feed.xml" }} Subscribe to the RSS feed for updates
|
||||
|
||||
|
@ -9,15 +9,14 @@
|
||||
|
||||
{{ .PostBody $post }}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
================================================================================
|
||||
|
||||
Published {{ $post.PublishedAt.Format "2006-01-02" }}
|
||||
Published {{ $post.PublishedAt.Format "2006-01-02" }} by mediocregopher
|
||||
|
||||
{{- if $post.Series }}
|
||||
|
||||
This post is part of a series!
|
||||
|
||||
{{ $seriesNextPrev := .GetPostSeriesNextPrevious $post -}}
|
||||
{{ if or $seriesNextPrev.Next $seriesNextPrev.Previous }}
|
||||
This post is part of a series!
|
||||
|
||||
{{ if $seriesNextPrev.Next -}}
|
||||
=> {{ 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 }}
|
||||
{{ end -}}
|
||||
|
||||
{{ end -}}
|
||||
{{ end }}
|
||||
=> {{ BlogURL "posts/" }} Browse all posts
|
||||
|
||||
|
@ -16,6 +16,9 @@ import (
|
||||
"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
|
||||
|
||||
// SetRequestLogger sets the given Logger onto the given Request's Context,
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
"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/http/apiutil"
|
||||
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
|
||||
@ -33,6 +33,7 @@ var staticFS embed.FS
|
||||
type Params struct {
|
||||
Logger *mlog.Logger
|
||||
PowManager pow.Manager
|
||||
Cache cache.Cache
|
||||
|
||||
PostStore post.Store
|
||||
PostAssetStore post.AssetStore
|
||||
@ -56,12 +57,17 @@ type Params struct {
|
||||
// AuthRatelimit indicates how much time must pass between subsequent auth
|
||||
// attempts.
|
||||
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.
|
||||
func (p *Params) SetupCfg(cfg *cfg.Cfg) {
|
||||
|
||||
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.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)
|
||||
}
|
||||
|
||||
if *geminiGatewayURLStr != "" {
|
||||
if p.GeminiGatewayURL, err = url.Parse(*geminiGatewayURLStr); err != nil {
|
||||
return fmt.Errorf("parsing -http-gemini-gateway-url: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@ -179,13 +191,6 @@ func (a *api) apiHandler() 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.Handle("/posts/", http.StripPrefix("/posts",
|
||||
@ -233,11 +238,11 @@ func (a *api) blogHandler() http.Handler {
|
||||
|
||||
readOnlyMiddlewares := []middleware{
|
||||
logReqMiddleware, // only log GETs on cache miss
|
||||
cacheMiddleware(cache),
|
||||
cacheMiddleware(a.params.Cache, a.params.PublicURL),
|
||||
}
|
||||
|
||||
readWriteMiddlewares := []middleware{
|
||||
purgeCacheOnOKMiddleware(cache),
|
||||
purgeCacheOnOKMiddleware(a.params.Cache),
|
||||
authMiddleware(a.auther),
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,12 @@ import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"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/mediocre-go-lib/v2/mctx"
|
||||
"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 {
|
||||
|
||||
type logCtxKey string
|
||||
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
wrw := newWrappedResponseWriter(rw)
|
||||
@ -94,8 +98,6 @@ func logReqMiddleware(h http.Handler) http.Handler {
|
||||
h.ServeHTTP(wrw, r)
|
||||
took := time.Since(started)
|
||||
|
||||
type logCtxKey string
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = mctx.Annotate(ctx,
|
||||
logCtxKey("took"), took.String(),
|
||||
@ -139,7 +141,7 @@ func (rw *cacheResponseWriter) Write(b []byte) (int, error) {
|
||||
return rw.wrappedResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func cacheMiddleware(cache *lru.Cache) middleware {
|
||||
func cacheMiddleware(cache cache.Cache, publicURL *url.URL) middleware {
|
||||
|
||||
type entry struct {
|
||||
body []byte
|
||||
@ -153,11 +155,14 @@ func cacheMiddleware(cache *lru.Cache) middleware {
|
||||
return func(h http.Handler) http.Handler {
|
||||
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)
|
||||
defer pool.Put(reader)
|
||||
@ -174,7 +179,7 @@ func cacheMiddleware(cache *lru.Cache) middleware {
|
||||
h.ServeHTTP(cacheRW, r)
|
||||
|
||||
if cacheRW.statusCode == 200 {
|
||||
cache.Add(id, entry{
|
||||
cache.Set(id, entry{
|
||||
body: cacheRW.buf.Bytes(),
|
||||
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 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 {
|
||||
return a.blogURL(path, false)
|
||||
},
|
||||
BlogHTTPURL: func(path string) string {
|
||||
return a.blogURL(path, false)
|
||||
},
|
||||
AssetURL: func(id string) string {
|
||||
return a.assetURL(id, false)
|
||||
},
|
||||
@ -96,7 +99,11 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload,
|
||||
prevBodyBuf := bodyBuf
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
{{ if ge .Payload.PrevPage 0 }}
|
||||
<p>
|
||||
<a href="?p={{ .Payload.PrevPage}}">< < Previous Page</a>
|
||||
<a href="?method=manage&p={{ .Payload.PrevPage}}">< < Previous Page</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
|
||||
{{ if ge .Payload.NextPage 0 }}
|
||||
<p>
|
||||
<a href="?p={{ .Payload.NextPage}}">Next Page > ></a>
|
||||
<a href="?method=manage&p={{ .Payload.NextPage}}">Next Page > ></a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
<ul>
|
||||
<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>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>
|
||||
|
||||
<h2>Dev</h2>
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
{{ if ge .Payload.PrevPage 0 }}
|
||||
<p>
|
||||
<a href="?p={{ .Payload.PrevPage}}">< < Previous Page</a>
|
||||
<a href="?method=manage&p={{ .Payload.PrevPage}}">< < Previous Page</a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
|
||||
{{ if ge .Payload.NextPage 0 }}
|
||||
<p>
|
||||
<a href="?p={{ .Payload.NextPage}}">Next Page > ></a>
|
||||
<a href="?method=manage&p={{ .Payload.NextPage}}">Next Page > ></a>
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
|
@ -17,6 +17,12 @@ type PreprocessFunctions struct {
|
||||
// The given path should not have a leading slash.
|
||||
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 func(id string) string
|
||||
|
||||
@ -39,11 +45,12 @@ type PreprocessFunctions struct {
|
||||
|
||||
func (funcs PreprocessFunctions) ToFuncsMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"BlogURL": funcs.BlogURL,
|
||||
"AssetURL": funcs.AssetURL,
|
||||
"PostURL": funcs.PostURL,
|
||||
"StaticURL": funcs.StaticURL,
|
||||
"Image": funcs.Image,
|
||||
"BlogURL": funcs.BlogURL,
|
||||
"BlogHTTPURL": funcs.BlogHTTPURL,
|
||||
"AssetURL": funcs.AssetURL,
|
||||
"PostURL": funcs.PostURL,
|
||||
"StaticURL": funcs.StaticURL,
|
||||
"Image": funcs.Image,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user