mirror of https://github.com/jlelse/GoBlog
Simple blogging system written in Go
https://goblog.app
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
192 lines
5.4 KiB
192 lines
5.4 KiB
package main |
|
|
|
import ( |
|
"context" |
|
"crypto/sha256" |
|
"errors" |
|
"fmt" |
|
"image/png" |
|
"io" |
|
"log" |
|
"net/http" |
|
|
|
"github.com/carlmjohnson/requests" |
|
"github.com/disintegration/imaging" |
|
"go.goblog.app/app/pkgs/bufferpool" |
|
) |
|
|
|
const defaultCompressionWidth = 2000 |
|
const defaultCompressionHeight = 3000 |
|
|
|
type mediaCompression interface { |
|
compress(url string, save mediaStorageSaveFunc, hc *http.Client) (location string, err error) |
|
} |
|
|
|
func (a *goBlog) compressMediaFile(url string) (location string, err error) { |
|
// Init compressors |
|
a.compressorsInit.Do(a.initMediaCompressors) |
|
// Try all compressors until success |
|
for _, c := range a.compressors { |
|
location, err = c.compress(url, a.saveMediaFile, a.httpClient) |
|
if location != "" && err == nil { |
|
break |
|
} |
|
} |
|
// Return result |
|
return location, err |
|
} |
|
|
|
func (a *goBlog) initMediaCompressors() { |
|
if a.cfg.Micropub == nil || a.cfg.Micropub.MediaStorage == nil { |
|
return |
|
} |
|
config := a.cfg.Micropub.MediaStorage |
|
if key := config.TinifyKey; key != "" { |
|
a.compressors = append(a.compressors, &tinify{key}) |
|
} |
|
if config.CloudflareCompressionEnabled { |
|
a.compressors = append(a.compressors, &cloudflare{}) |
|
} |
|
if config.LocalCompressionEnabled { |
|
a.compressors = append(a.compressors, &localMediaCompressor{}) |
|
} |
|
} |
|
|
|
type tinify struct { |
|
key string |
|
} |
|
|
|
func (tf *tinify) compress(url string, upload mediaStorageSaveFunc, hc *http.Client) (string, error) { |
|
tinifyErr := errors.New("failed to compress image using tinify") |
|
// Check url |
|
fileExtension, allowed := urlHasExt(url, "jpg", "jpeg", "png") |
|
if !allowed { |
|
return "", nil |
|
} |
|
// Compress |
|
headers := http.Header{} |
|
err := requests. |
|
URL("https://api.tinify.com/shrink"). |
|
Client(hc). |
|
Method(http.MethodPost). |
|
BasicAuth("api", tf.key). |
|
BodyJSON(map[string]any{ |
|
"source": map[string]any{ |
|
"url": url, |
|
}, |
|
}). |
|
ToHeaders(headers). |
|
Fetch(context.Background()) |
|
if err != nil { |
|
log.Println("Tinify error:", err.Error()) |
|
return "", tinifyErr |
|
} |
|
compressedLocation := headers.Get("Location") |
|
if compressedLocation == "" { |
|
log.Println("Tinify error: location header missing") |
|
return "", tinifyErr |
|
} |
|
// Resize and download image |
|
imgBuffer := bufferpool.Get() |
|
defer bufferpool.Put(imgBuffer) |
|
err = requests. |
|
URL(compressedLocation). |
|
Client(hc). |
|
Method(http.MethodPost). |
|
BasicAuth("api", tf.key). |
|
BodyJSON(map[string]any{ |
|
"resize": map[string]any{ |
|
"method": "fit", |
|
"width": defaultCompressionWidth, |
|
"height": defaultCompressionHeight, |
|
}, |
|
}). |
|
ToBytesBuffer(imgBuffer). |
|
Fetch(context.Background()) |
|
if err != nil { |
|
log.Println("Tinify error:", err.Error()) |
|
return "", tinifyErr |
|
} |
|
// Upload compressed file |
|
return uploadCompressedFile(fileExtension, imgBuffer, upload) |
|
} |
|
|
|
type cloudflare struct{} |
|
|
|
func (*cloudflare) compress(url string, upload mediaStorageSaveFunc, hc *http.Client) (string, error) { |
|
// Check url |
|
if _, allowed := urlHasExt(url, "jpg", "jpeg", "png"); !allowed { |
|
return "", nil |
|
} |
|
// Force jpeg |
|
fileExtension := "jpeg" |
|
// Compress |
|
imgBuffer := bufferpool.Get() |
|
defer bufferpool.Put(imgBuffer) |
|
err := requests. |
|
URL(fmt.Sprintf("https://www.cloudflare.com/cdn-cgi/image/f=jpeg,q=75,metadata=none,fit=scale-down,w=%d,h=%d/%s", defaultCompressionWidth, defaultCompressionHeight, url)). |
|
Client(hc). |
|
ToBytesBuffer(imgBuffer). |
|
Fetch(context.Background()) |
|
if err != nil { |
|
log.Println("Cloudflare error:", err.Error()) |
|
return "", errors.New("failed to compress image using cloudflare") |
|
} |
|
// Upload compressed file |
|
return uploadCompressedFile(fileExtension, imgBuffer, upload) |
|
} |
|
|
|
type localMediaCompressor struct{} |
|
|
|
func (*localMediaCompressor) compress(url string, upload mediaStorageSaveFunc, hc *http.Client) (string, error) { |
|
// Check url |
|
fileExtension, allowed := urlHasExt(url, "jpg", "jpeg", "png") |
|
if !allowed { |
|
return "", nil |
|
} |
|
// Download image |
|
imgBuffer := bufferpool.Get() |
|
defer bufferpool.Put(imgBuffer) |
|
err := requests. |
|
URL(url). |
|
Client(hc). |
|
ToBytesBuffer(imgBuffer). |
|
Fetch(context.Background()) |
|
if err != nil { |
|
log.Println("Local compressor error:", err.Error()) |
|
return "", errors.New("failed to download image using local compressor") |
|
} |
|
// Decode image |
|
img, err := imaging.Decode(imgBuffer, imaging.AutoOrientation(true)) |
|
if err != nil { |
|
log.Println("Local compressor error:", err.Error()) |
|
return "", errors.New("failed to compress image using local compressor") |
|
} |
|
// Resize image |
|
resizedImage := imaging.Fit(img, defaultCompressionWidth, defaultCompressionHeight, imaging.Lanczos) |
|
// Encode image |
|
resizedBuffer := bufferpool.Get() |
|
defer bufferpool.Put(resizedBuffer) |
|
switch fileExtension { |
|
case "jpg", "jpeg": |
|
err = imaging.Encode(resizedBuffer, resizedImage, imaging.JPEG, imaging.JPEGQuality(75)) |
|
case "png": |
|
err = imaging.Encode(resizedBuffer, resizedImage, imaging.PNG, imaging.PNGCompressionLevel(png.BestCompression)) |
|
} |
|
if err != nil { |
|
log.Println("Local compressor error:", err.Error()) |
|
return "", errors.New("failed to compress image using local compressor") |
|
} |
|
// Upload compressed file |
|
return uploadCompressedFile(fileExtension, resizedBuffer, upload) |
|
} |
|
|
|
func uploadCompressedFile(fileExtension string, r io.Reader, upload mediaStorageSaveFunc) (string, error) { |
|
// Copy file to temporary buffer to generate hash and filename |
|
hash := sha256.New() |
|
tempBuffer := bufferpool.Get() |
|
defer bufferpool.Put(tempBuffer) |
|
_, _ = io.Copy(io.MultiWriter(tempBuffer, hash), r) |
|
// Upload buffer |
|
return upload(fmt.Sprintf("%x.%s", hash.Sum(nil), fileExtension), tempBuffer) |
|
}
|
|
|