Micropub media endpoint

This commit is contained in:
Jan-Lukas Else 2020-10-14 21:20:17 +02:00
parent 5f0fdf2b5d
commit a3668baf10
6 changed files with 192 additions and 26 deletions

View File

@ -2,6 +2,7 @@ package main
import (
"errors"
"strings"
"github.com/spf13/viper"
)
@ -117,13 +118,21 @@ type frontmatter struct {
}
type configMicropub struct {
CategoryParam string `mapstructure:"categoryParam"`
ReplyParam string `mapstructure:"replyParam"`
LikeParam string `mapstructure:"likeParam"`
BookmarkParam string `mapstructure:"bookmarkParam"`
AudioParam string `mapstructure:"audioParam"`
PhotoParam string `mapstructure:"photoParam"`
PhotoDescriptionParam string `mapstructure:"photoDescriptionParam"`
CategoryParam string `mapstructure:"categoryParam"`
ReplyParam string `mapstructure:"replyParam"`
LikeParam string `mapstructure:"likeParam"`
BookmarkParam string `mapstructure:"bookmarkParam"`
AudioParam string `mapstructure:"audioParam"`
PhotoParam string `mapstructure:"photoParam"`
PhotoDescriptionParam string `mapstructure:"photoDescriptionParam"`
MediaStorage *configMicropubMedia `mapstructure:"mediaStorage"`
}
type configMicropubMedia struct {
MediaURL string `mapstructure:"mediaUrl"`
BunnyStorageKey string `mapstructure:"bunnyStorageKey"`
BunnyStorageName string `mapstructure:"bunnyStorageName"`
TinifyKey string `mapstructure:"tinifyKey"`
}
var appConfig = &config{}
@ -172,5 +181,14 @@ func initConfig() error {
if len(appConfig.DefaultBlog) == 0 || appConfig.Blogs[appConfig.DefaultBlog] == nil {
return errors.New("no default blog or default blog not present")
}
if appConfig.Micropub.MediaStorage != nil {
if appConfig.Micropub.MediaStorage.MediaURL == "" ||
appConfig.Micropub.MediaStorage.BunnyStorageKey == "" ||
appConfig.Micropub.MediaStorage.BunnyStorageName == "" {
appConfig.Micropub.MediaStorage = nil
} else {
appConfig.Micropub.MediaStorage.MediaURL = strings.TrimSuffix(appConfig.Micropub.MediaStorage.MediaURL, "/")
}
}
return nil
}

7
go.mod
View File

@ -3,6 +3,7 @@ module git.jlel.se/jlelse/GoBlog
go 1.15
require (
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0
github.com/PuerkitoBio/goquery v1.6.0
github.com/andybalholm/cascadia v1.2.0 // indirect
github.com/araddon/dateparse v0.0.0-20201001162425-8aadafed4dc4
@ -22,7 +23,7 @@ require (
github.com/lopezator/migrator v0.3.0
github.com/magiconair/properties v1.8.4 // indirect
github.com/mattn/go-sqlite3 v1.14.4
github.com/miekg/dns v1.1.32 // indirect
github.com/miekg/dns v1.1.33 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
@ -43,8 +44,8 @@ require (
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb // indirect
golang.org/x/sync v0.0.0-20201008141435-b3e1573b7520 // indirect
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb // indirect
golang.org/x/tools v0.0.0-20201013183236-0112737ef124 // indirect
golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc // indirect
golang.org/x/tools v0.0.0-20201014170642-d1624618ad65 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect

14
go.sum
View File

@ -10,6 +10,8 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0 h1:pJX79kTd01NtxEnzhfd4OU2SY9fquKVoO47DUeNKe+8=
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0/go.mod h1:X6cM4Sn0aL/4VQ/ku11yxmiV0WIk5XAaYEPHQLQjFFM=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -194,8 +196,8 @@ github.com/mholt/acmez v0.1.1 h1:KQODCqk+hBn3O7qfCRPj6L96uG65T5BSS95FKNEqtdA=
github.com/mholt/acmez v0.1.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.32 h1:MDaYYzWOYscpvDOEgPMT1c1mebCZmIdxZI/J161OdJU=
github.com/miekg/dns v1.1.32/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.33 h1:8KUVEKrUw2dmu1Ys0aWnkEJgoRaLAzNysfCh2KSMWiI=
github.com/miekg/dns v1.1.33/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@ -414,8 +416,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb h1:HS9IzC4UFbpMBLQUDSQcU+ViVT1vdFCQVjdPVpTlZrs=
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc h1:HVFDs9bKvTxP6bh1Rj9MCSo+UmafQtI8ZWDPVwVk9g4=
golang.org/x/sys v0.0.0-20201014080544-cc95f250f6bc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@ -447,8 +449,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201013183236-0112737ef124 h1:CQRvWGvGfDDk3OMpDX19jvU16tKf5RlqlJc+ki94ZJs=
golang.org/x/tools v0.0.0-20201013183236-0112737ef124/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201014170642-d1624618ad65 h1:q80OtYaeeySe8Kqg0vjXehHwj5fUTqe3xOvnbi5w3Gg=
golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -85,7 +85,9 @@ func buildHandler() (http.Handler, error) {
mpRouter.Use(middleware.NoCache, checkIndieAuth)
mpRouter.Get("/", serveMicropubQuery)
mpRouter.Post("/", serveMicropubPost)
mpRouter.Post(micropubMediaSubPath, serveMicropubMedia)
if appConfig.Micropub.MediaStorage != nil {
mpRouter.Post(micropubMediaSubPath, serveMicropubMedia)
}
})
// IndieAuth

View File

@ -15,7 +15,6 @@ import (
)
const micropubPath = "/micropub"
const micropubMediaSubPath = "/media"
type micropubConfig struct {
MediaEndpoint string `json:"media-endpoint,omitempty"`
@ -26,10 +25,11 @@ func serveMicropubQuery(w http.ResponseWriter, r *http.Request) {
case "config":
w.Header().Add(contentType, contentTypeJSON)
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(&micropubConfig{
// TODO: Uncomment when media endpoint implemented
// MediaEndpoint: appConfig.Server.PublicAddress + micropubMediaPath,
})
mc := &micropubConfig{}
if appConfig.Micropub.MediaStorage != nil {
mc.MediaEndpoint = appConfig.Server.PublicAddress + micropubPath + micropubMediaSubPath
}
_ = json.NewEncoder(w).Encode(mc)
case "source":
var mf interface{}
if urlString := r.URL.Query().Get("url"); urlString != "" {
@ -516,7 +516,3 @@ func micropubUpdate(w http.ResponseWriter, r *http.Request, u *url.URL, mf *micr
return
}
}
func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
// TODO: Implement media server
}

147
micropubMedia.go Normal file
View File

@ -0,0 +1,147 @@
package main
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
tfgo "codeberg.org/jlelse/tinify"
)
const micropubMediaSubPath = "/media"
func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Context().Value("scope").(string), "media") {
http.Error(w, "media scope missing", http.StatusForbidden)
}
if appConfig.Micropub.MediaStorage == nil {
http.Error(w, "Not configured", http.StatusNotImplemented)
return
}
if ct := r.Header.Get(contentType); !strings.Contains(ct, contentTypeMultipartForm) {
http.Error(w, "wrong content-type", http.StatusBadRequest)
return
}
err := r.ParseMultipartForm(0)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer func() { _ = file.Close() }()
hashFile, _, _ := r.FormFile("file")
defer func() { _ = hashFile.Close() }()
fileName, err := getSHA256(hashFile)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fileExtension := filepath.Ext(header.Filename)
if len(fileExtension) == 0 {
// Find correct file extension if original filename does not contain one
mimeType := header.Header.Get(contentType)
if len(mimeType) > 0 {
allExtensions, _ := mime.ExtensionsByType(mimeType)
if len(allExtensions) > 0 {
fileExtension = allExtensions[0]
}
}
}
fileName += strings.ToLower(fileExtension)
location, err := appConfig.Micropub.MediaStorage.uploadToBunny(fileName, file)
if err != nil {
http.Error(w, "failed to upload original file: "+err.Error(), http.StatusInternalServerError)
return
}
if appConfig.Micropub.MediaStorage.TinifyKey != "" {
compressedLocation, err := appConfig.Micropub.MediaStorage.tinify(location)
if err != nil {
http.Error(w, "failed to compress file: "+err.Error(), http.StatusInternalServerError)
return
} else if compressedLocation != "" {
location = compressedLocation
}
}
w.Header().Add("Location", location)
w.WriteHeader(http.StatusCreated)
}
func (mediaConf *configMicropubMedia) uploadToBunny(filename string, file multipart.File) (location string, err error) {
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("https://storage.bunnycdn.com/%s/%s", url.PathEscape(mediaConf.BunnyStorageName), url.PathEscape(filename)), file)
req.Header.Add("AccessKey", mediaConf.BunnyStorageKey)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != http.StatusCreated {
return "", errors.New("failed to upload file to BunnyCDN")
}
return mediaConf.MediaURL + "/" + filename, nil
}
func (mediaConf *configMicropubMedia) tinify(url string) (location string, err error) {
fileExtension := func() string {
spliced := strings.Split(url, ".")
return spliced[len(spliced)-1]
}()
supportedTypes := []string{"jpg", "jpeg", "png"}
sort.Strings(supportedTypes)
i := sort.SearchStrings(supportedTypes, strings.ToLower(fileExtension))
if !(i < len(supportedTypes) && supportedTypes[i] == strings.ToLower(fileExtension)) {
return "", nil
}
tfgo.SetKey(mediaConf.TinifyKey)
s, err := tfgo.FromUrl(url)
if err != nil {
return "", err
}
err = s.Resize(&tfgo.ResizeOption{
Method: tfgo.ResizeMethodScale,
Width: 2000,
})
if err != nil {
return "", err
}
file, err := ioutil.TempFile("", "tiny-*."+fileExtension)
if err != nil {
return "", err
}
defer func() {
_ = file.Close()
_ = os.Remove(file.Name())
}()
err = s.ToFile(file.Name())
if err != nil {
return "", err
}
hashFile, err := os.Open(file.Name())
defer func() { _ = hashFile.Close() }()
if err != nil {
return "", err
}
fileName, err := getSHA256(hashFile)
if err != nil {
return "", err
}
location, err = mediaConf.uploadToBunny(fileName+"."+fileExtension, file)
return
}
func getSHA256(file multipart.File) (filename string, err error) {
h := sha256.New()
if _, err = io.Copy(h, file); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}