Skip to content

Commit b67c79c

Browse files
committedFeb 6, 2025··
feat: micropub media endpoint
1 parent 8c7c54b commit b67c79c

File tree

7 files changed

+163
-2
lines changed

7 files changed

+163
-2
lines changed
 

‎go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/google/uuid v1.6.0
1313
github.com/karlseguin/typed v1.1.8
1414
github.com/lestrrat-go/jwx/v2 v2.1.3
15+
github.com/maypok86/otter v1.2.4
1516
github.com/meilisearch/meilisearch-go v0.30.0
1617
github.com/microcosm-cc/bluemonday v1.0.27
1718
github.com/robfig/cron/v3 v3.0.1
@@ -38,7 +39,9 @@ require (
3839
github.com/aymerick/douceur v0.2.0 // indirect
3940
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
4041
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
42+
github.com/dolthub/maphash v0.1.0 // indirect
4143
github.com/fsnotify/fsnotify v1.8.0 // indirect
44+
github.com/gammazero/deque v0.2.1 // indirect
4245
github.com/goccy/go-json v0.10.5 // indirect
4346
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
4447
github.com/gorilla/css v1.0.1 // indirect

‎go.sum

+6
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
114114
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
115115
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
116116
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
117+
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
118+
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
117119
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
118120
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
119121
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -137,6 +139,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
137139
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
138140
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
139141
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
142+
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
143+
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
140144
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
141145
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
142146
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
@@ -346,6 +350,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
346350
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
347351
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
348352
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
353+
github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
354+
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
349355
github.com/meilisearch/meilisearch-go v0.30.0 h1:J5TKZmfNOQc065+icxN2ShzT8u9F2/v6/gO/4DEw2ek=
350356
github.com/meilisearch/meilisearch-go v0.30.0/go.mod h1:NYOgjEGt/+oExD+NixreBMqxtIB0kCndXOOgpGhoqEs=
351357
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=

‎server/micropub.go

+74
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package server
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
78
"net/http"
9+
"path/filepath"
810
"strings"
911
"time"
1012

@@ -57,6 +59,10 @@ func (s *Server) makeMicropub() http.Handler {
5759
}))
5860
}
5961

62+
if s.media != nil {
63+
options = append(options, micropub.WithMediaEndpoint(s.c.AbsoluteURL(micropubMediaPath)))
64+
}
65+
6066
return micropub.NewHandler(&micropubServer{s: s}, options...)
6167
}
6268

@@ -380,6 +386,11 @@ func (m *micropubServer) updateEntryWithProps(e *core.Entry, newProps map[string
380386
}
381387
}
382388

389+
err := m.updateEntryWithPhotos(e, properties)
390+
if err != nil {
391+
return err
392+
}
393+
383394
for _, k := range m.s.c.Micropub.Properties {
384395
if v, ok := properties[k]; ok {
385396
e.Other[k] = v
@@ -394,3 +405,66 @@ func (m *micropubServer) updateEntryWithProps(e *core.Entry, newProps map[string
394405

395406
return nil
396407
}
408+
409+
func (m *micropubServer) updateEntryWithPhotos(e *core.Entry, properties typed.Typed) error {
410+
parts := strings.Split(strings.TrimSuffix(e.ID, "/"), "/")
411+
slug := parts[len(parts)-1]
412+
prefix := fmt.Sprintf("%04d-%02d-%s", e.Date.Year(), e.Date.Month(), slug)
413+
414+
photoUrls := []string{}
415+
photoData := map[string][]byte{}
416+
417+
if url, ok := properties.StringIf("photo"); ok {
418+
data, ok := m.s.mediaCache.Get(url)
419+
if !ok {
420+
return fmt.Errorf("photo %q not found in cache", url)
421+
}
422+
423+
photoUrls = append(photoUrls, url)
424+
photoData[url] = data
425+
m.s.mediaCache.Delete(url)
426+
427+
delete(properties, "photo")
428+
} else if photos, ok := properties.StringsIf("photo"); ok {
429+
for _, url := range photos {
430+
data, ok := m.s.mediaCache.Get(url)
431+
if !ok {
432+
return fmt.Errorf("photo %q not found in cache", url)
433+
}
434+
435+
photoUrls = append(photoUrls, url)
436+
photoData[url] = data
437+
m.s.mediaCache.Delete(url)
438+
}
439+
440+
delete(properties, "photo")
441+
}
442+
443+
if len(photoUrls) == 0 {
444+
return nil
445+
}
446+
447+
photos := []any{}
448+
449+
for i, url := range photoUrls {
450+
data := photoData[url]
451+
filename := prefix
452+
if len(photoUrls) > 1 {
453+
filename += fmt.Sprintf("-%02d", i+1)
454+
}
455+
456+
ext := filepath.Ext(url)
457+
cdnUrl, err := m.s.media.UploadMedia(filename, ext, bytes.NewBuffer(data))
458+
if err != nil {
459+
return fmt.Errorf("failed to upload photo: %w", err)
460+
}
461+
462+
photos = append(photos, map[string]string{
463+
"url": cdnUrl,
464+
})
465+
}
466+
467+
e.Other["photos"] = photos
468+
469+
return nil
470+
}

‎server/micropub_media.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package server
2+
3+
import (
4+
"crypto/sha256"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"mime/multipart"
9+
"net/http"
10+
"path/filepath"
11+
12+
"github.com/gabriel-vasile/mimetype"
13+
"github.com/samber/lo"
14+
"go.hacdias.com/indielib/micropub"
15+
)
16+
17+
const (
18+
micropubMediaPath = "/micropub/media"
19+
)
20+
21+
func (s *Server) makeMicropubMedia() http.Handler {
22+
return micropub.NewMediaHandler(func(file multipart.File, header *multipart.FileHeader) (string, error) {
23+
data, err := io.ReadAll(file)
24+
if err != nil {
25+
return "", err
26+
}
27+
28+
ext := filepath.Ext(header.Filename)
29+
if ext == "" {
30+
// NOTE: I'm not using http.DetectContentType because it depends
31+
// on OS specific mime type registries. Thus, it was being unreliable
32+
// on different OSes.
33+
contentType := header.Header.Get("Content-Type")
34+
mime := mimetype.Lookup(contentType)
35+
if mime.Is("application/octet-stream") {
36+
mime = mimetype.Detect(data)
37+
}
38+
39+
if mime == nil {
40+
return "", errors.New("cannot deduce mimetype")
41+
}
42+
43+
ext = mime.Extension()
44+
}
45+
46+
filename := fmt.Sprintf("cache://%x%s", sha256.Sum256(data), ext)
47+
48+
added := s.mediaCache.Set(filename, data)
49+
if !added {
50+
return "", errors.New("failed to add item to cache")
51+
}
52+
53+
return filename, nil
54+
}, func(r *http.Request, scope string) bool {
55+
return lo.Contains(s.getScopes(r), scope)
56+
})
57+
}

‎server/router.go

+3
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ func (s *Server) makeRouter() http.Handler {
9797
// Micropub
9898
if s.c.Micropub != nil {
9999
r.Handle(micropubPath, s.makeMicropub())
100+
if s.media != nil {
101+
r.Handle(micropubMediaPath, s.makeMicropubMedia())
102+
}
100103
}
101104
})
102105

‎server/server.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"time"
2121

2222
"github.com/go-chi/jwtauth/v5"
23+
"github.com/maypok86/otter"
2324
"github.com/robfig/cron/v3"
2425
"github.com/samber/lo"
2526
"go.hacdias.com/eagle/core"
@@ -62,8 +63,11 @@ type Server struct {
6263
onionAddress string
6364
meilisearch *meilisearch.MeiliSearch
6465
core *core.Core
65-
media *media.Media
66-
bolt *database.Database
66+
67+
media *media.Media
68+
mediaCache *otter.Cache[string, []byte]
69+
70+
bolt *database.Database
6771

6872
staticFsLock sync.RWMutex
6973
staticFs *staticFs
@@ -97,6 +101,7 @@ func NewServer(c *core.Config) (*Server, error) {
97101
co.BuildHook = s.buildHook
98102

99103
err = errors.Join(
104+
s.initMediaCache(),
100105
s.initNotifier(),
101106
s.initTemplates(),
102107
s.initBolt(),

‎server/server_init.go

+13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"html/template"
66
"net/url"
77
"path/filepath"
8+
"time"
89

10+
"github.com/maypok86/otter"
911
"go.hacdias.com/eagle/core"
1012
"go.hacdias.com/eagle/log"
1113
"go.hacdias.com/eagle/services/bunny"
@@ -33,6 +35,17 @@ func initMedia(c *core.Config) *media.Media {
3335
return nil
3436
}
3537

38+
func (s *Server) initMediaCache() error {
39+
cache, err := otter.MustBuilder[string, []byte](1e8).
40+
WithTTL(time.Hour).
41+
Cost(func(key string, value []byte) uint32 {
42+
return uint32(len(value))
43+
}).
44+
Build()
45+
46+
s.mediaCache = &cache
47+
return err
48+
}
3649
func (s *Server) initNotifier() error {
3750
var err error
3851
if s.c.Notifications.Telegram != nil {

0 commit comments

Comments
 (0)
Please sign in to comment.