2020-10-14 19:20:17 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-02-09 18:30:07 +00:00
|
|
|
"bytes"
|
2020-10-14 19:20:17 +00:00
|
|
|
"crypto/sha256"
|
2021-02-09 18:30:07 +00:00
|
|
|
"encoding/json"
|
2020-10-14 19:20:17 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"mime"
|
|
|
|
"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) {
|
2021-02-08 17:51:07 +00:00
|
|
|
if !strings.Contains(r.Context().Value(indieAuthScope).(string), "media") {
|
2020-12-24 09:09:34 +00:00
|
|
|
serveError(w, r, "media scope missing", http.StatusForbidden)
|
2020-11-22 19:30:02 +00:00
|
|
|
return
|
2020-10-14 19:20:17 +00:00
|
|
|
}
|
|
|
|
if ct := r.Header.Get(contentType); !strings.Contains(ct, contentTypeMultipartForm) {
|
2020-12-24 09:09:34 +00:00
|
|
|
serveError(w, r, "wrong content-type", http.StatusBadRequest)
|
2020-10-14 19:20:17 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
err := r.ParseMultipartForm(0)
|
|
|
|
if err != nil {
|
2020-12-24 09:09:34 +00:00
|
|
|
serveError(w, r, err.Error(), http.StatusBadRequest)
|
2020-10-14 19:20:17 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
file, header, err := r.FormFile("file")
|
|
|
|
if err != nil {
|
2020-12-24 09:09:34 +00:00
|
|
|
serveError(w, r, err.Error(), http.StatusBadRequest)
|
2020-10-14 19:20:17 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer func() { _ = file.Close() }()
|
|
|
|
hashFile, _, _ := r.FormFile("file")
|
|
|
|
defer func() { _ = hashFile.Close() }()
|
|
|
|
fileName, err := getSHA256(hashFile)
|
|
|
|
if err != nil {
|
2020-12-24 09:09:34 +00:00
|
|
|
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
2020-10-14 19:20:17 +00:00
|
|
|
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)
|
2021-01-10 14:59:43 +00:00
|
|
|
// Save file
|
|
|
|
location, err := uploadFile(fileName, file)
|
2020-10-14 19:20:17 +00:00
|
|
|
if err != nil {
|
2021-01-10 14:59:43 +00:00
|
|
|
serveError(w, r, "failed to save original file: "+err.Error(), http.StatusInternalServerError)
|
2020-10-14 19:20:17 +00:00
|
|
|
return
|
|
|
|
}
|
2021-01-10 14:59:43 +00:00
|
|
|
// Try to compress file
|
2021-02-09 18:30:07 +00:00
|
|
|
if ms := appConfig.Micropub.MediaStorage; ms != nil {
|
|
|
|
serveCompressionError := func(ce error) {
|
|
|
|
serveError(w, r, "failed to compress file: "+ce.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
|
|
|
|
}
|
|
|
|
// Fallback Tinify
|
|
|
|
if compressedLocation == "" && ms.TinifyKey != "" {
|
|
|
|
compressedLocation, compressionErr = tinify(location, ms)
|
|
|
|
}
|
|
|
|
if compressionErr != nil {
|
|
|
|
serveCompressionError(compressionErr)
|
2020-10-14 19:20:17 +00:00
|
|
|
return
|
2021-02-09 18:30:07 +00:00
|
|
|
}
|
|
|
|
if compressedLocation != "" {
|
2020-10-14 19:20:17 +00:00
|
|
|
location = compressedLocation
|
|
|
|
}
|
|
|
|
}
|
2020-12-13 10:28:46 +00:00
|
|
|
http.Redirect(w, r, location, http.StatusCreated)
|
2020-10-14 19:20:17 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 14:59:43 +00:00
|
|
|
func uploadFile(filename string, f io.Reader) (string, error) {
|
|
|
|
ms := appConfig.Micropub.MediaStorage
|
|
|
|
if ms != nil && ms.BunnyStorageKey != "" && ms.BunnyStorageName != "" {
|
|
|
|
return uploadToBunny(filename, f, ms)
|
|
|
|
}
|
|
|
|
loc, err := saveMediaFile(filename, f)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if ms != nil && ms.MediaURL != "" {
|
|
|
|
return ms.MediaURL + loc, nil
|
|
|
|
}
|
2021-02-09 18:30:07 +00:00
|
|
|
return appConfig.Server.PublicAddress + loc, nil
|
2021-01-10 14:59:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func uploadToBunny(filename string, f io.Reader, config *configMicropubMedia) (location string, err error) {
|
2021-02-09 18:30:07 +00:00
|
|
|
if config == nil || config.BunnyStorageName == "" || config.BunnyStorageKey == "" || config.MediaURL == "" {
|
|
|
|
return "", errors.New("Bunny storage not completely configured")
|
|
|
|
}
|
2021-01-10 14:59:43 +00:00
|
|
|
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)
|
2020-10-14 19:20:17 +00:00
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusCreated {
|
|
|
|
return "", errors.New("failed to upload file to BunnyCDN")
|
|
|
|
}
|
2021-01-10 14:59:43 +00:00
|
|
|
return config.MediaURL + "/" + filename, nil
|
2020-10-14 19:20:17 +00:00
|
|
|
}
|
|
|
|
|
2021-01-10 14:59:43 +00:00
|
|
|
func tinify(url string, config *configMicropubMedia) (location string, err error) {
|
2021-02-09 18:30:07 +00:00
|
|
|
// Check config
|
|
|
|
if config == nil || config.TinifyKey == "" {
|
|
|
|
return "", errors.New("Tinify not configured")
|
|
|
|
}
|
|
|
|
// Check url
|
|
|
|
fileExtension, allowed := compressionIsSupported(url, "jpg", "jpeg", "png")
|
|
|
|
if !allowed {
|
2020-10-14 19:20:17 +00:00
|
|
|
return "", nil
|
|
|
|
}
|
2021-02-09 18:30:07 +00:00
|
|
|
// Compress
|
2021-01-10 14:59:43 +00:00
|
|
|
tfgo.SetKey(config.TinifyKey)
|
2020-10-14 19:20:17 +00:00
|
|
|
s, err := tfgo.FromUrl(url)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-02-09 18:30:07 +00:00
|
|
|
if err = s.Resize(&tfgo.ResizeOption{
|
2020-10-14 19:20:17 +00:00
|
|
|
Method: tfgo.ResizeMethodScale,
|
|
|
|
Width: 2000,
|
2021-02-09 18:30:07 +00:00
|
|
|
}); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-02-17 07:23:03 +00:00
|
|
|
tmpFile, err := os.CreateTemp("", "tiny-*."+fileExtension)
|
2021-02-09 18:30:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
_ = tmpFile.Close()
|
|
|
|
_ = os.Remove(tmpFile.Name())
|
|
|
|
}()
|
|
|
|
if err = s.ToFile(tmpFile.Name()); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-02-17 12:35:29 +00:00
|
|
|
fileName, err := getSHA256(tmpFile)
|
2021-02-09 18:30:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
// Upload compressed file
|
|
|
|
location, err = uploadFile(fileName+"."+fileExtension, tmpFile)
|
|
|
|
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
|
2021-02-17 12:35:29 +00:00
|
|
|
j, _ := json.Marshal(map[string]interface{}{
|
2021-02-09 18:30:07 +00:00
|
|
|
"key": config.ShortPixelKey,
|
|
|
|
"plugin_version": "GB001",
|
|
|
|
"lossy": 1,
|
|
|
|
"resize": 3,
|
|
|
|
"resize_width": 2000,
|
|
|
|
"resize_height": 3000,
|
|
|
|
"cmyk2rgb": 1,
|
|
|
|
"keep_exif": 0,
|
|
|
|
"url": url,
|
2020-10-14 19:20:17 +00:00
|
|
|
})
|
2021-02-17 12:35:29 +00:00
|
|
|
req, err := http.NewRequest(http.MethodPut, "https://api.shortpixel.com/v2/reducer-sync.php", bytes.NewReader(j))
|
2021-02-09 18:30:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
2020-10-14 19:20:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2021-02-09 18:30:07 +00:00
|
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
|
|
return "", fmt.Errorf("failed to compress image, status code %d", resp.StatusCode)
|
2020-10-14 19:20:17 +00:00
|
|
|
}
|
2021-02-17 07:23:03 +00:00
|
|
|
tmpFile, err := os.CreateTemp("", "tiny-*."+fileExtension)
|
2020-10-14 19:20:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
defer func() {
|
2021-02-09 18:30:07 +00:00
|
|
|
_ = resp.Body.Close()
|
|
|
|
_ = tmpFile.Close()
|
2021-02-17 12:35:29 +00:00
|
|
|
_ = os.Remove(tmpFile.Name())
|
2020-10-14 19:20:17 +00:00
|
|
|
}()
|
2021-02-09 18:30:07 +00:00
|
|
|
if _, err = io.Copy(tmpFile, resp.Body); err != nil {
|
2020-10-14 19:20:17 +00:00
|
|
|
return "", err
|
|
|
|
}
|
2021-02-17 12:35:29 +00:00
|
|
|
fileName, err := getSHA256(tmpFile)
|
2020-10-14 19:20:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-02-09 18:30:07 +00:00
|
|
|
// Upload compressed file
|
|
|
|
location, err = uploadFile(fileName+"."+fileExtension, tmpFile)
|
2020-10-14 19:20:17 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-09 18:30:07 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-02-17 12:35:29 +00:00
|
|
|
func getSHA256(file io.ReadSeeker) (filename string, err error) {
|
|
|
|
if _, err = file.Seek(0, 0); err != nil {
|
2020-10-14 19:20:17 +00:00
|
|
|
return "", err
|
|
|
|
}
|
2021-02-17 12:35:29 +00:00
|
|
|
h := sha256.New()
|
|
|
|
if _, err = io.Copy(h, file); err != nil {
|
2021-02-09 18:30:07 +00:00
|
|
|
return "", err
|
|
|
|
}
|
2021-02-17 12:35:29 +00:00
|
|
|
if _, err = file.Seek(0, 0); err != nil {
|
2021-02-09 18:30:07 +00:00
|
|
|
return "", err
|
|
|
|
}
|
2021-02-17 12:35:29 +00:00
|
|
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
2021-02-09 18:30:07 +00:00
|
|
|
}
|