mirror of
				https://github.com/imgproxy/imgproxy.git
				synced 2025-10-30 23:08:02 +02:00 
			
		
		
		
	raw processing option
				
					
				
			This commit is contained in:
		| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| ## [Unreleased] | ||||
| ### Add | ||||
| - Add [raw](https://docs.imgproxy.net/latest/generating_the_url?id=raw) processing option. | ||||
| - (pro) Add encrypted source URL support. | ||||
|  | ||||
| ## [3.7.2] - 2022-08-22 | ||||
|   | ||||
| @@ -23,6 +23,7 @@ RUN apt-get update \ | ||||
|     liblzma5 \ | ||||
|     libzstd1 \ | ||||
|     libpcre3 \ | ||||
|     media-types \ | ||||
|   && rm -rf /var/lib/apt/lists/* | ||||
|  | ||||
| COPY --from=0 /usr/local/bin/imgproxy /usr/local/bin/ | ||||
|   | ||||
| @@ -662,6 +662,21 @@ When set, imgproxy will skip the processing of the listed formats. Also availabl | ||||
|  | ||||
| Default: empty | ||||
|  | ||||
| ### Raw | ||||
|  | ||||
| ``` | ||||
| raw:%raw | ||||
| ``` | ||||
|  | ||||
| When set to `1`, `t` or `true`, imgproxy will respond with a raw unprocessed, and unchecked source image. There are some differences between `raw` and `skip_processing` options: | ||||
|  | ||||
| * While the `skip_processing` option has some conditions to skip the processing, the `raw` option allows to skip processing no matter what | ||||
| * With the `raw` option set, imgproxy doesn't check the source image's type, resolution, and file size. Basically, the `raw` option allows streaming of any file type | ||||
| * With the `raw` option set, imgproxy won't download the whole image to the memory. Instead, it will stream the source image directly to the response lowering memory usage | ||||
| * The requests with the `raw` option set are not limited by the `IMGPROXY_CONCURRENCY` config | ||||
|  | ||||
| Default: `false` | ||||
|  | ||||
| ### Cache buster | ||||
|  | ||||
| ``` | ||||
|   | ||||
| @@ -130,7 +130,7 @@ func headersToStore(res *http.Response) map[string]string { | ||||
| 	return m | ||||
| } | ||||
|  | ||||
| func requestImage(imageURL string, header http.Header, jar *cookiejar.Jar) (*http.Response, error) { | ||||
| func BuildImageRequest(imageURL string, header http.Header, jar *cookiejar.Jar) (*http.Request, error) { | ||||
| 	req, err := http.NewRequest("GET", imageURL, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, ierrors.New(404, err.Error(), msgSourceImageIsUnreachable) | ||||
| @@ -158,12 +158,31 @@ func requestImage(imageURL string, header http.Header, jar *cookiejar.Jar) (*htt | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return req, nil | ||||
| } | ||||
|  | ||||
| func SendRequest(req *http.Request) (*http.Response, error) { | ||||
| 	res, err := downloadClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, ierrors.New(500, checkTimeoutErr(err).Error(), msgSourceImageIsUnreachable) | ||||
| 	} | ||||
|  | ||||
| 	return res, nil | ||||
| } | ||||
|  | ||||
| func requestImage(imageURL string, header http.Header, jar *cookiejar.Jar) (*http.Response, error) { | ||||
| 	req, err := BuildImageRequest(imageURL, header, jar) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	res, err := SendRequest(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if res.StatusCode == http.StatusNotModified { | ||||
| 		res.Body.Close() | ||||
| 		return nil, &ErrorNotModified{Message: "Not Modified", Headers: headersToStore(res)} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -67,6 +67,15 @@ var ( | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func ByMime(mime string) Type { | ||||
| 	for k, v := range mimes { | ||||
| 		if v == mime { | ||||
| 			return k | ||||
| 		} | ||||
| 	} | ||||
| 	return Unknown | ||||
| } | ||||
|  | ||||
| func (it Type) String() string { | ||||
| 	for k, v := range Types { | ||||
| 		if v == it { | ||||
|   | ||||
| @@ -88,6 +88,20 @@ func StartProcessingSegment(ctx context.Context) context.CancelFunc { | ||||
| 	return cancel | ||||
| } | ||||
|  | ||||
| func StartStreamingSegment(ctx context.Context) context.CancelFunc { | ||||
| 	promCancel := prometheus.StartStreamingSegment() | ||||
| 	nrCancel := newrelic.StartSegment(ctx, "Streaming image") | ||||
| 	ddCancel := datadog.StartSpan(ctx, "streaming_image") | ||||
|  | ||||
| 	cancel := func() { | ||||
| 		promCancel() | ||||
| 		nrCancel() | ||||
| 		ddCancel() | ||||
| 	} | ||||
|  | ||||
| 	return cancel | ||||
| } | ||||
|  | ||||
| func SendError(ctx context.Context, errType string, err error) { | ||||
| 	prometheus.IncrementErrorsTotal(errType) | ||||
| 	newrelic.SendError(ctx, errType, err) | ||||
|   | ||||
| @@ -195,6 +195,14 @@ func StartProcessingSegment() context.CancelFunc { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func StartStreamingSegment() context.CancelFunc { | ||||
| 	if !enabled { | ||||
| 		return func() {} | ||||
| 	} | ||||
|  | ||||
| 	return startDuration(requestSpanDuration.With(prometheus.Labels{"span": "streaming"})) | ||||
| } | ||||
|  | ||||
| func startDuration(m prometheus.Observer) context.CancelFunc { | ||||
| 	t := time.Now() | ||||
| 	return func() { | ||||
|   | ||||
| @@ -103,6 +103,8 @@ type ProcessingOptions struct { | ||||
| 	Filename         string | ||||
| 	ReturnAttachment bool | ||||
|  | ||||
| 	Raw bool | ||||
|  | ||||
| 	UsedPresets []string | ||||
|  | ||||
| 	defaultQuality int | ||||
| @@ -775,6 +777,16 @@ func applySkipProcessingFormatsOption(po *ProcessingOptions, args []string) erro | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func applyRawOption(po *ProcessingOptions, args []string) error { | ||||
| 	if len(args) > 1 { | ||||
| 		return fmt.Errorf("Invalid return_attachment arguments: %v", args) | ||||
| 	} | ||||
|  | ||||
| 	po.Raw = parseBoolOption(args[0]) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func applyFilenameOption(po *ProcessingOptions, args []string) error { | ||||
| 	if len(args) > 1 { | ||||
| 		return fmt.Errorf("Invalid filename arguments: %v", args) | ||||
| @@ -928,6 +940,8 @@ func applyURLOption(po *ProcessingOptions, name string, args []string) error { | ||||
| 	// Handling options | ||||
| 	case "skip_processing", "skp": | ||||
| 		return applySkipProcessingFormatsOption(po, args) | ||||
| 	case "raw": | ||||
| 		return applyRawOption(po, args) | ||||
| 	case "cachebuster", "cb": | ||||
| 		return applyCacheBusterOption(po, args) | ||||
| 	case "expires", "exp": | ||||
|   | ||||
| @@ -61,10 +61,10 @@ func setCacheControl(rw http.ResponseWriter, originHeaders map[string]string) { | ||||
| 	var ttl int | ||||
|  | ||||
| 	if config.CacheControlPassthrough && originHeaders != nil { | ||||
| 		if val, ok := originHeaders["Cache-Control"]; ok { | ||||
| 		if val, ok := originHeaders["Cache-Control"]; ok && len(val) > 0 { | ||||
| 			cacheControl = val | ||||
| 		} | ||||
| 		if val, ok := originHeaders["Expires"]; ok { | ||||
| 		if val, ok := originHeaders["Expires"]; ok && len(val) > 0 { | ||||
| 			expires = val | ||||
| 		} | ||||
| 	} | ||||
| @@ -92,6 +92,15 @@ func setVary(rw http.ResponseWriter) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func setCanonical(rw http.ResponseWriter, originURL string) { | ||||
| 	if config.SetCanonicalHeader { | ||||
| 		if strings.HasPrefix(originURL, "https://") || strings.HasPrefix(originURL, "http://") { | ||||
| 			linkHeader := fmt.Sprintf(`<%s>; rel="canonical"`, originURL) | ||||
| 			rw.Header().Set("Link", linkHeader) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) { | ||||
| 	var contentDisposition string | ||||
| 	if len(po.Filename) > 0 { | ||||
| @@ -107,15 +116,9 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta | ||||
| 		rw.Header().Set("Content-DPR", strconv.FormatFloat(po.Dpr, 'f', 2, 32)) | ||||
| 	} | ||||
|  | ||||
| 	if config.SetCanonicalHeader { | ||||
| 		if strings.HasPrefix(originURL, "https://") || strings.HasPrefix(originURL, "http://") { | ||||
| 			linkHeader := fmt.Sprintf(`<%s>; rel="canonical"`, originURL) | ||||
| 			rw.Header().Set("Link", linkHeader) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	setCacheControl(rw, originData.Headers) | ||||
| 	setVary(rw) | ||||
| 	setCanonical(rw, originURL) | ||||
|  | ||||
| 	if config.EnableDebugHeaders { | ||||
| 		rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(len(originData.Data))) | ||||
| @@ -229,6 +232,11 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) { | ||||
| 		)) | ||||
| 	} | ||||
|  | ||||
| 	if po.Raw { | ||||
| 		streamOriginImage(ctx, reqID, r, rw, po, imageURL) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// SVG is a special case. Though saving to svg is not supported, SVG->SVG is. | ||||
| 	if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG { | ||||
| 		sendErrAndPanic(ctx, "path_parsing", ierrors.New( | ||||
|   | ||||
| @@ -133,6 +133,10 @@ func withPanicHandler(h router.RouteHandler) router.RouteHandler { | ||||
| 	return func(reqID string, rw http.ResponseWriter, r *http.Request) { | ||||
| 		defer func() { | ||||
| 			if rerr := recover(); rerr != nil { | ||||
| 				if rerr == http.ErrAbortHandler { | ||||
| 					panic(rerr) | ||||
| 				} | ||||
|  | ||||
| 				err, ok := rerr.(error) | ||||
| 				if !ok { | ||||
| 					panic(rerr) | ||||
|   | ||||
							
								
								
									
										142
									
								
								stream.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								stream.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"io" | ||||
| 	"mime" | ||||
| 	"net/http" | ||||
| 	"net/http/cookiejar" | ||||
| 	"net/url" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"sync" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/config" | ||||
| 	"github.com/imgproxy/imgproxy/v3/cookies" | ||||
| 	"github.com/imgproxy/imgproxy/v3/imagedata" | ||||
| 	"github.com/imgproxy/imgproxy/v3/imagetype" | ||||
| 	"github.com/imgproxy/imgproxy/v3/metrics" | ||||
| 	"github.com/imgproxy/imgproxy/v3/metrics/stats" | ||||
| 	"github.com/imgproxy/imgproxy/v3/options" | ||||
| 	"github.com/imgproxy/imgproxy/v3/router" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	streamReqHeaders = []string{ | ||||
| 		"If-None-Match", | ||||
| 		"Accept-Encoding", | ||||
| 		"Range", | ||||
| 	} | ||||
|  | ||||
| 	streamRespHeaders = []string{ | ||||
| 		"Cache-Control", | ||||
| 		"Expires", | ||||
| 		"ETag", | ||||
| 		"Content-Type", | ||||
| 		"Content-Encoding", | ||||
| 		"Content-Range", | ||||
| 	} | ||||
|  | ||||
| 	streamBufPool = sync.Pool{ | ||||
| 		New: func() interface{} { | ||||
| 			buf := make([]byte, 4096) | ||||
| 			return &buf | ||||
| 		}, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func streamOriginImage(ctx context.Context, reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, imageURL string) { | ||||
| 	stats.IncImagesInProgress() | ||||
| 	defer stats.DecImagesInProgress() | ||||
|  | ||||
| 	defer metrics.StartStreamingSegment(ctx)() | ||||
|  | ||||
| 	var ( | ||||
| 		cookieJar *cookiejar.Jar | ||||
| 		err       error | ||||
| 	) | ||||
|  | ||||
| 	imgRequestHeader := make(http.Header) | ||||
|  | ||||
| 	for _, k := range streamReqHeaders { | ||||
| 		if v := r.Header.Get(k); len(v) != 0 { | ||||
| 			imgRequestHeader.Set(k, v) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if config.CookiePassthrough { | ||||
| 		cookieJar, err = cookies.JarFromRequest(r) | ||||
| 		checkErr(ctx, "streaming", err) | ||||
| 	} | ||||
|  | ||||
| 	req, err := imagedata.BuildImageRequest(imageURL, imgRequestHeader, cookieJar) | ||||
| 	checkErr(ctx, "streaming", err) | ||||
|  | ||||
| 	res, err := imagedata.SendRequest(req) | ||||
| 	checkErr(ctx, "streaming", err) | ||||
|  | ||||
| 	defer res.Body.Close() | ||||
|  | ||||
| 	for _, k := range streamRespHeaders { | ||||
| 		vv := res.Header.Values(k) | ||||
| 		for _, v := range vv { | ||||
| 			rw.Header().Set(k, v) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if res.ContentLength >= 0 { | ||||
| 		rw.Header().Set("Content-Length", strconv.Itoa(int(res.ContentLength))) | ||||
| 	} | ||||
|  | ||||
| 	if res.StatusCode < 300 { | ||||
| 		imgtype := imagetype.Unknown | ||||
|  | ||||
| 		if mimetype := rw.Header().Get("Content-Type"); len(mimetype) > 0 { | ||||
| 			imgtype = imagetype.ByMime(mimetype) | ||||
| 		} else { | ||||
| 			if u, uerr := url.Parse(imageURL); uerr == nil { | ||||
| 				if ext := filepath.Ext(u.Path); len(ext) > 1 { | ||||
| 					imgtype = imagetype.Types[ext[1:]] | ||||
|  | ||||
| 					if mimetype := mime.TypeByExtension(ext); len(mimetype) > 0 { | ||||
| 						rw.Header().Set("Content-Type", mimetype) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var contentDisposition string | ||||
| 		if len(po.Filename) > 0 { | ||||
| 			contentDisposition = imgtype.ContentDisposition(po.Filename, po.ReturnAttachment) | ||||
| 		} else { | ||||
| 			contentDisposition = imgtype.ContentDispositionFromURL(imageURL, po.ReturnAttachment) | ||||
| 		} | ||||
|  | ||||
| 		rw.Header().Set("Content-Disposition", contentDisposition) | ||||
| 	} | ||||
|  | ||||
| 	setCacheControl(rw, map[string]string{ | ||||
| 		"Cache-Control": rw.Header().Get("Cache-Control"), | ||||
| 		"Expires":       rw.Header().Get("Expires"), | ||||
| 	}) | ||||
| 	setCanonical(rw, imageURL) | ||||
|  | ||||
| 	rw.WriteHeader(res.StatusCode) | ||||
|  | ||||
| 	buf := streamBufPool.Get().(*[]byte) | ||||
| 	defer streamBufPool.Put(buf) | ||||
|  | ||||
| 	if _, err := io.CopyBuffer(rw, res.Body, *buf); err != nil { | ||||
| 		panic(http.ErrAbortHandler) | ||||
| 	} | ||||
|  | ||||
| 	router.LogResponse( | ||||
| 		reqID, r, res.StatusCode, nil, | ||||
| 		log.Fields{ | ||||
| 			"image_url":          imageURL, | ||||
| 			"processing_options": po, | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user