Fix media endpoint, add ShortPixel as preferred compression method

This commit is contained in:
Jan-Lukas Else 2021-02-09 19:30:07 +01:00
parent d5b37eba73
commit 66ada4c0ae
2 changed files with 134 additions and 37 deletions

View File

@ -174,6 +174,7 @@ type configMicropubMedia struct {
BunnyStorageKey string `mapstructure:"bunnyStorageKey"` BunnyStorageKey string `mapstructure:"bunnyStorageKey"`
BunnyStorageName string `mapstructure:"bunnyStorageName"` BunnyStorageName string `mapstructure:"bunnyStorageName"`
TinifyKey string `mapstructure:"tinifyKey"` TinifyKey string `mapstructure:"tinifyKey"`
ShortPixelKey string `mapstructure:"shortPixelKey"`
} }
type configRegexRedirect struct { type configRegexRedirect struct {

View File

@ -1,7 +1,9 @@
package main package main
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -25,10 +27,6 @@ func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
serveError(w, r, "media scope missing", http.StatusForbidden) serveError(w, r, "media scope missing", http.StatusForbidden)
return return
} }
if appConfig.Micropub.MediaStorage == nil {
serveError(w, r, "Not configured", http.StatusNotImplemented)
return
}
if ct := r.Header.Get(contentType); !strings.Contains(ct, contentTypeMultipartForm) { if ct := r.Header.Get(contentType); !strings.Contains(ct, contentTypeMultipartForm) {
serveError(w, r, "wrong content-type", http.StatusBadRequest) serveError(w, r, "wrong content-type", http.StatusBadRequest)
return return
@ -70,15 +68,30 @@ func serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
return return
} }
// Try to compress file // Try to compress file
if ms := appConfig.Micropub.MediaStorage; ms != nil && ms.TinifyKey != "" { if ms := appConfig.Micropub.MediaStorage; ms != nil {
compressedLocation, err := tinify(location, ms) serveCompressionError := func(ce error) {
if err != nil { serveError(w, r, "failed to compress file: "+ce.Error(), http.StatusInternalServerError)
serveError(w, r, "failed to compress file: "+err.Error(), http.StatusInternalServerError) }
var compressedLocation string
var compressionErr error
// Default ShortPixel
if ms.ShortPixelKey != "" {
compressedLocation, compressionErr = shortPixel(location, ms)
}
if compressionErr != nil {
serveCompressionError(compressionErr)
return return
} else if compressedLocation != "" { }
// Fallback Tinify
if compressedLocation == "" && ms.TinifyKey != "" {
compressedLocation, compressionErr = tinify(location, ms)
}
if compressionErr != nil {
serveCompressionError(compressionErr)
return
}
if compressedLocation != "" {
location = compressedLocation location = compressedLocation
} else {
serveError(w, r, "No compressed location", http.StatusInternalServerError)
} }
} }
http.Redirect(w, r, location, http.StatusCreated) http.Redirect(w, r, location, http.StatusCreated)
@ -96,10 +109,13 @@ func uploadFile(filename string, f io.Reader) (string, error) {
if ms != nil && ms.MediaURL != "" { if ms != nil && ms.MediaURL != "" {
return ms.MediaURL + loc, nil return ms.MediaURL + loc, nil
} }
return loc, nil return appConfig.Server.PublicAddress + loc, nil
} }
func uploadToBunny(filename string, f io.Reader, config *configMicropubMedia) (location string, err error) { func uploadToBunny(filename string, f io.Reader, config *configMicropubMedia) (location string, err error) {
if config == nil || config.BunnyStorageName == "" || config.BunnyStorageKey == "" || config.MediaURL == "" {
return "", errors.New("Bunny storage not completely configured")
}
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("https://storage.bunnycdn.com/%s/%s", url.PathEscape(config.BunnyStorageName), url.PathEscape(filename)), f) req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("https://storage.bunnycdn.com/%s/%s", url.PathEscape(config.BunnyStorageName), url.PathEscape(filename)), f)
req.Header.Add("AccessKey", config.BunnyStorageKey) req.Header.Add("AccessKey", config.BunnyStorageKey)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
@ -110,53 +126,118 @@ func uploadToBunny(filename string, f io.Reader, config *configMicropubMedia) (l
} }
func tinify(url string, config *configMicropubMedia) (location string, err error) { func tinify(url string, config *configMicropubMedia) (location string, err error) {
fileExtension := func() string { // Check config
spliced := strings.Split(url, ".") if config == nil || config.TinifyKey == "" {
return spliced[len(spliced)-1] return "", errors.New("Tinify not configured")
}() }
supportedTypes := []string{"jpg", "jpeg", "png"} // Check url
sort.Strings(supportedTypes) fileExtension, allowed := compressionIsSupported(url, "jpg", "jpeg", "png")
i := sort.SearchStrings(supportedTypes, strings.ToLower(fileExtension)) if !allowed {
if !(i < len(supportedTypes) && supportedTypes[i] == strings.ToLower(fileExtension)) {
return "", nil return "", nil
} }
// Compress
tfgo.SetKey(config.TinifyKey) tfgo.SetKey(config.TinifyKey)
s, err := tfgo.FromUrl(url) s, err := tfgo.FromUrl(url)
if err != nil { if err != nil {
return "", err return "", err
} }
err = s.Resize(&tfgo.ResizeOption{ if err = s.Resize(&tfgo.ResizeOption{
Method: tfgo.ResizeMethodScale, Method: tfgo.ResizeMethodScale,
Width: 2000, Width: 2000,
}) }); err != nil {
if err != nil {
return "", err return "", err
} }
file, err := ioutil.TempFile("", "tiny-*."+fileExtension) tmpFile, err := ioutil.TempFile("", "tiny-*."+fileExtension)
if err != nil { if err != nil {
return "", err return "", err
} }
defer func() { defer func() {
_ = file.Close() _ = tmpFile.Close()
_ = os.Remove(file.Name()) _ = os.Remove(tmpFile.Name())
}() }()
err = s.ToFile(file.Name()) if err = s.ToFile(tmpFile.Name()); err != nil {
return "", err
}
fileName, err := hashFile(tmpFile.Name())
if err != nil { if err != nil {
return "", err return "", err
} }
hashFile, err := os.Open(file.Name()) // Upload compressed file
defer func() { _ = hashFile.Close() }() location, err = uploadFile(fileName+"."+fileExtension, tmpFile)
if err != nil {
return "", err
}
fileName, err := getSHA256(hashFile)
if err != nil {
return "", err
}
location, err = uploadFile(fileName+"."+fileExtension, file)
return return
} }
func shortPixel(url string, config *configMicropubMedia) (location string, err error) {
// Check config
if config == nil || config.ShortPixelKey == "" {
return "", errors.New("ShortPixel not configured")
}
// Check url
fileExtension, allowed := compressionIsSupported(url, "jpg", "jpeg", "png")
if !allowed {
return "", nil
}
// Compress
var buf bytes.Buffer
_ = json.NewEncoder(&buf).Encode(map[string]interface{}{
"key": config.ShortPixelKey,
"plugin_version": "GB001",
"lossy": 1,
"resize": 3,
"resize_width": 2000,
"resize_height": 3000,
"cmyk2rgb": 1,
"keep_exif": 0,
"url": url,
})
req, err := http.NewRequest(http.MethodPut, "https://api.shortpixel.com/v2/reducer-sync.php", &buf)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
} else if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to compress image, status code %d", resp.StatusCode)
}
tmpFile, err := ioutil.TempFile("", "tiny-*."+fileExtension)
if err != nil {
return "", err
}
tmpFileName := tmpFile.Name()
defer func() {
_ = resp.Body.Close()
_ = tmpFile.Close()
_ = os.Remove(tmpFileName)
}()
if _, err = io.Copy(tmpFile, resp.Body); err != nil {
return "", err
}
fileName, err := hashFile(tmpFileName)
if err != nil {
return "", err
}
// Reopen tmp file
_ = tmpFile.Close()
tmpFile, err = os.Open(tmpFileName)
if err != nil {
return "", err
}
// Upload compressed file
location, err = uploadFile(fileName+"."+fileExtension, tmpFile)
return
}
func compressionIsSupported(url string, allowed ...string) (string, bool) {
spliced := strings.Split(url, ".")
ext := spliced[len(spliced)-1]
sort.Strings(allowed)
if i := sort.SearchStrings(allowed, strings.ToLower(ext)); i >= len(allowed) || allowed[i] != strings.ToLower(ext) {
return ext, false
}
return ext, true
}
func getSHA256(file multipart.File) (filename string, err error) { func getSHA256(file multipart.File) (filename string, err error) {
h := sha256.New() h := sha256.New()
if _, err = io.Copy(h, file); err != nil { if _, err = io.Copy(h, file); err != nil {
@ -164,3 +245,18 @@ func getSHA256(file multipart.File) (filename string, err error) {
} }
return fmt.Sprintf("%x", h.Sum(nil)), nil return fmt.Sprintf("%x", h.Sum(nil)), nil
} }
func hashFile(filename string) (string, error) {
hashFile, err := os.Open(filename)
if err != nil {
return "", err
}
defer func() {
_ = hashFile.Close()
}()
fn, err := getSHA256(hashFile)
if err != nil {
return "", err
}
return fn, nil
}