mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-01-08 10:45:04 +02:00
Extract processing handler and imageType functions from server.go
This commit is contained in:
parent
86c883f04b
commit
a9244a7063
96
image_type.go
Normal file
96
image_type.go
Normal file
@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -s -w
|
||||
#include "vips.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type imageType int
|
||||
|
||||
const (
|
||||
imageTypeUnknown = imageType(C.UNKNOWN)
|
||||
imageTypeJPEG = imageType(C.JPEG)
|
||||
imageTypePNG = imageType(C.PNG)
|
||||
imageTypeWEBP = imageType(C.WEBP)
|
||||
imageTypeGIF = imageType(C.GIF)
|
||||
imageTypeICO = imageType(C.ICO)
|
||||
imageTypeSVG = imageType(C.SVG)
|
||||
imageTypeHEIC = imageType(C.HEIC)
|
||||
|
||||
contentDispositionFilenameFallback = "image"
|
||||
)
|
||||
|
||||
var (
|
||||
imageTypes = map[string]imageType{
|
||||
"jpeg": imageTypeJPEG,
|
||||
"jpg": imageTypeJPEG,
|
||||
"png": imageTypePNG,
|
||||
"webp": imageTypeWEBP,
|
||||
"gif": imageTypeGIF,
|
||||
"ico": imageTypeICO,
|
||||
"svg": imageTypeSVG,
|
||||
"heic": imageTypeHEIC,
|
||||
}
|
||||
|
||||
mimes = map[imageType]string{
|
||||
imageTypeJPEG: "image/jpeg",
|
||||
imageTypePNG: "image/png",
|
||||
imageTypeWEBP: "image/webp",
|
||||
imageTypeGIF: "image/gif",
|
||||
imageTypeICO: "image/x-icon",
|
||||
imageTypeHEIC: "image/heif",
|
||||
}
|
||||
|
||||
contentDispositionsFmt = map[imageType]string{
|
||||
imageTypeJPEG: "inline; filename=\"%s.jpg\"",
|
||||
imageTypePNG: "inline; filename=\"%s.png\"",
|
||||
imageTypeWEBP: "inline; filename=\"%s.webp\"",
|
||||
imageTypeGIF: "inline; filename=\"%s.gif\"",
|
||||
imageTypeICO: "inline; filename=\"%s.ico\"",
|
||||
imageTypeHEIC: "inline; filename=\"%s.heic\"",
|
||||
}
|
||||
)
|
||||
|
||||
func (it imageType) String() string {
|
||||
for k, v := range imageTypes {
|
||||
if v == it {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (it imageType) Mime() string {
|
||||
if mime, ok := mimes[it]; ok {
|
||||
return mime
|
||||
} else {
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
func (it imageType) ContentDisposition(imageURL string) string {
|
||||
format, ok := contentDispositionsFmt[it]
|
||||
if !ok {
|
||||
return "inline"
|
||||
}
|
||||
|
||||
url, err := url.Parse(imageURL)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(format, contentDispositionFilenameFallback)
|
||||
}
|
||||
|
||||
_, filename := filepath.Split(url.Path)
|
||||
if len(filename) == 0 {
|
||||
return fmt.Sprintf(format, contentDispositionFilenameFallback)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(format, strings.TrimSuffix(filename, filepath.Ext(filename)))
|
||||
}
|
149
processing_handler.go
Normal file
149
processing_handler.go
Normal file
@ -0,0 +1,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
responseGzipBufPool *bufPool
|
||||
responseGzipPool *gzipPool
|
||||
|
||||
processingSem chan struct{}
|
||||
|
||||
headerVaryValue string
|
||||
)
|
||||
|
||||
func initProcessingHandler() {
|
||||
processingSem = make(chan struct{}, conf.Concurrency)
|
||||
|
||||
if conf.GZipCompression > 0 {
|
||||
responseGzipBufPool = newBufPool("gzip", conf.Concurrency, conf.GZipBufferSize)
|
||||
responseGzipPool = newGzipPool(conf.Concurrency)
|
||||
}
|
||||
|
||||
vary := make([]string, 0)
|
||||
|
||||
if conf.EnableWebpDetection || conf.EnforceWebp {
|
||||
vary = append(vary, "Accept")
|
||||
}
|
||||
|
||||
if conf.GZipCompression > 0 {
|
||||
vary = append(vary, "Accept-Encoding")
|
||||
}
|
||||
|
||||
if conf.EnableClientHints {
|
||||
vary = append(vary, "DPR", "Viewport-Width", "Width")
|
||||
}
|
||||
|
||||
headerVaryValue = strings.Join(vary, ", ")
|
||||
}
|
||||
|
||||
func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, data []byte) {
|
||||
po := getProcessingOptions(ctx)
|
||||
|
||||
rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))
|
||||
rw.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL))
|
||||
rw.Header().Set("Content-Type", po.Format.Mime())
|
||||
rw.Header().Set("Content-Disposition", po.Format.ContentDisposition(getImageURL(ctx)))
|
||||
|
||||
if len(headerVaryValue) > 0 {
|
||||
rw.Header().Set("Vary", headerVaryValue)
|
||||
}
|
||||
|
||||
if conf.GZipCompression > 0 && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
buf := responseGzipBufPool.Get(0)
|
||||
defer responseGzipBufPool.Put(buf)
|
||||
|
||||
gz := responseGzipPool.Get(buf)
|
||||
defer responseGzipPool.Put(gz)
|
||||
|
||||
gz.Write(data)
|
||||
gz.Close()
|
||||
|
||||
rw.Header().Set("Content-Encoding", "gzip")
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(buf.Bytes())
|
||||
} else {
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(data)
|
||||
}
|
||||
|
||||
logResponse(reqID, 200, fmt.Sprintf("Processed in %s: %s; %+v", getTimerSince(ctx), getImageURL(ctx), po))
|
||||
}
|
||||
|
||||
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
|
||||
if newRelicEnabled {
|
||||
var newRelicCancel context.CancelFunc
|
||||
ctx, newRelicCancel = startNewRelicTransaction(ctx, rw, r)
|
||||
defer newRelicCancel()
|
||||
}
|
||||
|
||||
if prometheusEnabled {
|
||||
prometheusRequestsTotal.Inc()
|
||||
defer startPrometheusDuration(prometheusRequestDuration)()
|
||||
}
|
||||
|
||||
processingSem <- struct{}{}
|
||||
defer func() { <-processingSem }()
|
||||
|
||||
ctx, timeoutCancel := startTimer(ctx, time.Duration(conf.WriteTimeout)*time.Second)
|
||||
defer timeoutCancel()
|
||||
|
||||
ctx, err := parsePath(ctx, r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ctx, downloadcancel, err := downloadImage(ctx)
|
||||
defer downloadcancel()
|
||||
if err != nil {
|
||||
if newRelicEnabled {
|
||||
sendErrorToNewRelic(ctx, err)
|
||||
}
|
||||
if prometheusEnabled {
|
||||
incrementPrometheusErrorsTotal("download")
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
checkTimeout(ctx)
|
||||
|
||||
if conf.ETagEnabled {
|
||||
eTag := calcETag(ctx)
|
||||
rw.Header().Set("ETag", eTag)
|
||||
|
||||
if eTag == r.Header.Get("If-None-Match") {
|
||||
logResponse(reqID, 304, "Not modified")
|
||||
rw.WriteHeader(304)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
checkTimeout(ctx)
|
||||
|
||||
imageData, processcancel, err := processImage(ctx)
|
||||
defer processcancel()
|
||||
if err != nil {
|
||||
if newRelicEnabled {
|
||||
sendErrorToNewRelic(ctx, err)
|
||||
}
|
||||
if prometheusEnabled {
|
||||
incrementPrometheusErrorsTotal("processing")
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
checkTimeout(ctx)
|
||||
|
||||
respondWithImage(ctx, reqID, r, rw, imageData)
|
||||
}
|
@ -1,11 +1,5 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -s -w
|
||||
#include "vips.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
@ -20,19 +14,6 @@ import (
|
||||
|
||||
type urlOptions map[string][]string
|
||||
|
||||
type imageType int
|
||||
|
||||
const (
|
||||
imageTypeUnknown = imageType(C.UNKNOWN)
|
||||
imageTypeJPEG = imageType(C.JPEG)
|
||||
imageTypePNG = imageType(C.PNG)
|
||||
imageTypeWEBP = imageType(C.WEBP)
|
||||
imageTypeGIF = imageType(C.GIF)
|
||||
imageTypeICO = imageType(C.ICO)
|
||||
imageTypeSVG = imageType(C.SVG)
|
||||
imageTypeHEIC = imageType(C.HEIC)
|
||||
)
|
||||
|
||||
type processingHeaders struct {
|
||||
Accept string
|
||||
Width string
|
||||
@ -40,17 +21,6 @@ type processingHeaders struct {
|
||||
DPR string
|
||||
}
|
||||
|
||||
var imageTypes = map[string]imageType{
|
||||
"jpeg": imageTypeJPEG,
|
||||
"jpg": imageTypeJPEG,
|
||||
"png": imageTypePNG,
|
||||
"webp": imageTypeWEBP,
|
||||
"gif": imageTypeGIF,
|
||||
"ico": imageTypeICO,
|
||||
"svg": imageTypeSVG,
|
||||
"heic": imageTypeHEIC,
|
||||
}
|
||||
|
||||
type gravityType int
|
||||
|
||||
const (
|
||||
@ -158,15 +128,6 @@ var (
|
||||
errInvalidPath = newError(404, "Invalid path", msgInvalidURL)
|
||||
)
|
||||
|
||||
func (it imageType) String() string {
|
||||
for k, v := range imageTypes {
|
||||
if v == it {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (gt gravityType) String() string {
|
||||
for k, v := range gravityTypes {
|
||||
if v == gt {
|
||||
|
209
server.go
209
server.go
@ -6,47 +6,15 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/netutil"
|
||||
)
|
||||
|
||||
const (
|
||||
contextDispositionFilenameFallback = "image"
|
||||
)
|
||||
|
||||
var (
|
||||
mimes = map[imageType]string{
|
||||
imageTypeJPEG: "image/jpeg",
|
||||
imageTypePNG: "image/png",
|
||||
imageTypeWEBP: "image/webp",
|
||||
imageTypeGIF: "image/gif",
|
||||
imageTypeICO: "image/x-icon",
|
||||
imageTypeHEIC: "image/heif",
|
||||
}
|
||||
|
||||
contentDispositionsFmt = map[imageType]string{
|
||||
imageTypeJPEG: "inline; filename=\"%s.jpg\"",
|
||||
imageTypePNG: "inline; filename=\"%s.png\"",
|
||||
imageTypeWEBP: "inline; filename=\"%s.webp\"",
|
||||
imageTypeGIF: "inline; filename=\"%s.gif\"",
|
||||
imageTypeICO: "inline; filename=\"%s.ico\"",
|
||||
imageTypeHEIC: "inline; filename=\"%s.heic\"",
|
||||
}
|
||||
|
||||
imgproxyIsRunningMsg = []byte("imgproxy is running")
|
||||
|
||||
errInvalidMethod = newError(422, "Invalid request method", "Method doesn't allowed")
|
||||
errInvalidSecret = newError(403, "Invalid secret", "Forbidden")
|
||||
|
||||
responseGzipBufPool *bufPool
|
||||
responseGzipPool *gzipPool
|
||||
|
||||
processingSem chan struct{}
|
||||
)
|
||||
|
||||
func buildRouter() *router {
|
||||
@ -62,8 +30,6 @@ func buildRouter() *router {
|
||||
}
|
||||
|
||||
func startServer() *http.Server {
|
||||
processingSem = make(chan struct{}, conf.Concurrency)
|
||||
|
||||
l, err := net.Listen("tcp", conf.Bind)
|
||||
if err != nil {
|
||||
logFatal(err.Error())
|
||||
@ -76,10 +42,7 @@ func startServer() *http.Server {
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
if conf.GZipCompression > 0 {
|
||||
responseGzipBufPool = newBufPool("gzip", conf.Concurrency, conf.GZipBufferSize)
|
||||
responseGzipPool = newGzipPool(conf.Concurrency)
|
||||
}
|
||||
initProcessingHandler()
|
||||
|
||||
go func() {
|
||||
logNotice("Starting server at %s", conf.Bind)
|
||||
@ -100,86 +63,6 @@ func shutdownServer(s *http.Server) {
|
||||
s.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func contentDisposition(imageURL string, imgtype imageType) string {
|
||||
url, err := url.Parse(imageURL)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(contentDispositionsFmt[imgtype], contextDispositionFilenameFallback)
|
||||
}
|
||||
|
||||
_, filename := filepath.Split(url.Path)
|
||||
if len(filename) == 0 {
|
||||
return fmt.Sprintf(contentDispositionsFmt[imgtype], contextDispositionFilenameFallback)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(contentDispositionsFmt[imgtype], strings.TrimSuffix(filename, filepath.Ext(filename)))
|
||||
}
|
||||
|
||||
func respondWithImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, data []byte) {
|
||||
po := getProcessingOptions(ctx)
|
||||
|
||||
rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))
|
||||
rw.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, public", conf.TTL))
|
||||
rw.Header().Set("Content-Type", mimes[po.Format])
|
||||
rw.Header().Set("Content-Disposition", contentDisposition(getImageURL(ctx), po.Format))
|
||||
|
||||
addVaryHeader(rw)
|
||||
|
||||
if conf.GZipCompression > 0 && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
buf := responseGzipBufPool.Get(0)
|
||||
defer responseGzipBufPool.Put(buf)
|
||||
|
||||
gz := responseGzipPool.Get(buf)
|
||||
defer responseGzipPool.Put(gz)
|
||||
|
||||
gz.Write(data)
|
||||
gz.Close()
|
||||
|
||||
rw.Header().Set("Content-Encoding", "gzip")
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(buf.Bytes())
|
||||
} else {
|
||||
rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
rw.WriteHeader(200)
|
||||
rw.Write(data)
|
||||
}
|
||||
|
||||
logResponse(reqID, 200, fmt.Sprintf("Processed in %s: %s; %+v", getTimerSince(ctx), getImageURL(ctx), po))
|
||||
}
|
||||
|
||||
func addVaryHeader(rw http.ResponseWriter) {
|
||||
vary := make([]string, 0)
|
||||
|
||||
if conf.EnableWebpDetection || conf.EnforceWebp {
|
||||
vary = append(vary, "Accept")
|
||||
}
|
||||
|
||||
if conf.GZipCompression > 0 {
|
||||
vary = append(vary, "Accept-Encoding")
|
||||
}
|
||||
|
||||
if conf.EnableClientHints {
|
||||
vary = append(vary, "DPR", "Viewport-Width", "Width")
|
||||
}
|
||||
|
||||
if len(vary) > 0 {
|
||||
rw.Header().Set("Vary", strings.Join(vary, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func respondWithError(reqID string, rw http.ResponseWriter, err *imgproxyError) {
|
||||
logResponse(reqID, err.StatusCode, err.Message)
|
||||
|
||||
rw.WriteHeader(err.StatusCode)
|
||||
|
||||
if conf.DevelopmentErrorsMode {
|
||||
rw.Write([]byte(err.Message))
|
||||
} else {
|
||||
rw.Write([]byte(err.PublicMessage))
|
||||
}
|
||||
}
|
||||
|
||||
func withCORS(h routeHandler) routeHandler {
|
||||
return func(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
if len(conf.AllowOrigin) > 0 {
|
||||
@ -202,7 +85,7 @@ func withSecret(h routeHandler) routeHandler {
|
||||
if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), authHeader) == 1 {
|
||||
h(reqID, rw, r)
|
||||
} else {
|
||||
respondWithError(reqID, rw, errInvalidSecret)
|
||||
panic(errInvalidSecret)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,10 +93,23 @@ func withSecret(h routeHandler) routeHandler {
|
||||
func handlePanic(reqID string, rw http.ResponseWriter, r *http.Request, err error) {
|
||||
reportError(err, r)
|
||||
|
||||
if ierr, ok := err.(*imgproxyError); ok {
|
||||
respondWithError(reqID, rw, ierr)
|
||||
var (
|
||||
ierr *imgproxyError
|
||||
ok bool
|
||||
)
|
||||
|
||||
if ierr, ok = err.(*imgproxyError); !ok {
|
||||
ierr = newUnexpectedError(err.Error(), 3)
|
||||
}
|
||||
|
||||
logResponse(reqID, ierr.StatusCode, ierr.Message)
|
||||
|
||||
rw.WriteHeader(ierr.StatusCode)
|
||||
|
||||
if conf.DevelopmentErrorsMode {
|
||||
rw.Write([]byte(ierr.Message))
|
||||
} else {
|
||||
respondWithError(reqID, rw, newUnexpectedError(err.Error(), 3))
|
||||
rw.Write([]byte(ierr.PublicMessage))
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,72 +123,3 @@ func handleOptions(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
logResponse(reqID, 200, "Respond with options")
|
||||
rw.WriteHeader(200)
|
||||
}
|
||||
|
||||
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
|
||||
if newRelicEnabled {
|
||||
var newRelicCancel context.CancelFunc
|
||||
ctx, newRelicCancel = startNewRelicTransaction(ctx, rw, r)
|
||||
defer newRelicCancel()
|
||||
}
|
||||
|
||||
if prometheusEnabled {
|
||||
prometheusRequestsTotal.Inc()
|
||||
defer startPrometheusDuration(prometheusRequestDuration)()
|
||||
}
|
||||
|
||||
processingSem <- struct{}{}
|
||||
defer func() { <-processingSem }()
|
||||
|
||||
ctx, timeoutCancel := startTimer(ctx, time.Duration(conf.WriteTimeout)*time.Second)
|
||||
defer timeoutCancel()
|
||||
|
||||
ctx, err := parsePath(ctx, r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ctx, downloadcancel, err := downloadImage(ctx)
|
||||
defer downloadcancel()
|
||||
if err != nil {
|
||||
if newRelicEnabled {
|
||||
sendErrorToNewRelic(ctx, err)
|
||||
}
|
||||
if prometheusEnabled {
|
||||
incrementPrometheusErrorsTotal("download")
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
checkTimeout(ctx)
|
||||
|
||||
if conf.ETagEnabled {
|
||||
eTag := calcETag(ctx)
|
||||
rw.Header().Set("ETag", eTag)
|
||||
|
||||
if eTag == r.Header.Get("If-None-Match") {
|
||||
logResponse(reqID, 304, "Not modified")
|
||||
rw.WriteHeader(304)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
checkTimeout(ctx)
|
||||
|
||||
imageData, processcancel, err := processImage(ctx)
|
||||
defer processcancel()
|
||||
if err != nil {
|
||||
if newRelicEnabled {
|
||||
sendErrorToNewRelic(ctx, err)
|
||||
}
|
||||
if prometheusEnabled {
|
||||
incrementPrometheusErrorsTotal("processing")
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
checkTimeout(ctx)
|
||||
|
||||
respondWithImage(ctx, reqID, r, rw, imageData)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user