1
0
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:
DarthSim 2019-06-03 23:02:46 +06:00
parent 86c883f04b
commit a9244a7063
4 changed files with 263 additions and 230 deletions

96
image_type.go Normal file
View 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
View 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)
}

View File

@ -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
View File

@ -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)
}