mirror of https://github.com/jlelse/GoBlog
Micropub media endpoint
This commit is contained in:
parent
5f0fdf2b5d
commit
a3668baf10
32
config.go
32
config.go
|
@ -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
7
go.mod
|
@ -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
14
go.sum
|
@ -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=
|
||||
|
|
4
http.go
4
http.go
|
@ -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
|
||||
|
|
14
micropub.go
14
micropub.go
|
@ -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(µpubConfig{
|
||||
// TODO: Uncomment when media endpoint implemented
|
||||
// MediaEndpoint: appConfig.Server.PublicAddress + micropubMediaPath,
|
||||
})
|
||||
mc := µpubConfig{}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue