mirror of
				https://github.com/imgproxy/imgproxy.git
				synced 2025-10-30 23:08:02 +02:00 
			
		
		
		
	Add ranged requests suport to transports
This commit is contained in:
		
							
								
								
									
										73
									
								
								httprange/httprange.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								httprange/httprange.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| package httprange | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"net/textproto" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func Parse(s string) (int64, int64, error) { | ||||
| 	if s == "" { | ||||
| 		return 0, 0, nil // header not present | ||||
| 	} | ||||
|  | ||||
| 	const b = "bytes=" | ||||
| 	if !strings.HasPrefix(s, b) { | ||||
| 		return 0, 0, errors.New("invalid range") | ||||
| 	} | ||||
|  | ||||
| 	for _, ra := range strings.Split(s[len(b):], ",") { | ||||
| 		ra = textproto.TrimString(ra) | ||||
| 		if ra == "" { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		i := strings.Index(ra, "-") | ||||
| 		if i < 0 { | ||||
| 			return 0, 0, errors.New("invalid range") | ||||
| 		} | ||||
|  | ||||
| 		start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:]) | ||||
|  | ||||
| 		if start == "" { | ||||
| 			// Don't support ranges without start since it looks like FFmpeg doen't use ones | ||||
| 			return 0, 0, errors.New("invalid range") | ||||
| 		} | ||||
|  | ||||
| 		istart, err := strconv.ParseInt(start, 10, 64) | ||||
| 		if err != nil || i < 0 { | ||||
| 			return 0, 0, errors.New("invalid range") | ||||
| 		} | ||||
|  | ||||
| 		var iend int64 | ||||
|  | ||||
| 		if end == "" { | ||||
| 			iend = -1 | ||||
| 		} else { | ||||
| 			iend, err = strconv.ParseInt(end, 10, 64) | ||||
| 			if err != nil || istart > iend { | ||||
| 				return 0, 0, errors.New("invalid range") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return istart, iend, nil | ||||
| 	} | ||||
|  | ||||
| 	return 0, 0, errors.New("invalid range") | ||||
| } | ||||
|  | ||||
| func InvalidHTTPRangeResponse(req *http.Request) *http.Response { | ||||
| 	return &http.Response{ | ||||
| 		StatusCode:    http.StatusRequestedRangeNotSatisfiable, | ||||
| 		Proto:         "HTTP/1.0", | ||||
| 		ProtoMajor:    1, | ||||
| 		ProtoMinor:    0, | ||||
| 		Header:        make(http.Header), | ||||
| 		ContentLength: 0, | ||||
| 		Body:          nil, | ||||
| 		Close:         false, | ||||
| 		Request:       req, | ||||
| 	} | ||||
| } | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
|  | ||||
| 	"github.com/Azure/azure-storage-blob-go/azblob" | ||||
| 	"github.com/imgproxy/imgproxy/v3/config" | ||||
| 	"github.com/imgproxy/imgproxy/v3/httprange" | ||||
| ) | ||||
|  | ||||
| type transport struct { | ||||
| @@ -40,12 +41,22 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) | ||||
| 	containerURL := t.serviceURL.NewContainerURL(strings.ToLower(req.URL.Host)) | ||||
| 	blobURL := containerURL.NewBlockBlobURL(strings.TrimPrefix(req.URL.Path, "/")) | ||||
|  | ||||
| 	get, err := blobURL.Download(req.Context(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) | ||||
| 	start, end, err := httprange.Parse(req.Header.Get("Range")) | ||||
| 	if err != nil { | ||||
| 		return httprange.InvalidHTTPRangeResponse(req), nil | ||||
| 	} | ||||
|  | ||||
| 	length := end - start + 1 | ||||
| 	if end <= 0 { | ||||
| 		length = azblob.CountToEnd | ||||
| 	} | ||||
|  | ||||
| 	get, err := blobURL.Download(req.Context(), start, length, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if config.ETagEnabled { | ||||
| 	if config.ETagEnabled && start == 0 && end == azblob.CountToEnd { | ||||
| 		etag := string(get.ETag()) | ||||
|  | ||||
| 		if etag == req.Header.Get("If-None-Match") { | ||||
|   | ||||
							
								
								
									
										27
									
								
								transport/fs/file_limiter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								transport/fs/file_limiter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| package fs | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type fileLimiter struct { | ||||
| 	f    http.File | ||||
| 	left int | ||||
| } | ||||
|  | ||||
| func (lr *fileLimiter) Read(p []byte) (n int, err error) { | ||||
| 	if lr.left <= 0 { | ||||
| 		return 0, io.EOF | ||||
| 	} | ||||
| 	if len(p) > lr.left { | ||||
| 		p = p[0:lr.left] | ||||
| 	} | ||||
| 	n, err = lr.f.Read(p) | ||||
| 	lr.left -= n | ||||
| 	return | ||||
| } | ||||
|  | ||||
| func (lr *fileLimiter) Close() error { | ||||
| 	return lr.f.Close() | ||||
| } | ||||
| @@ -6,11 +6,15 @@ import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/fs" | ||||
| 	"mime" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/config" | ||||
| 	"github.com/imgproxy/imgproxy/v3/httprange" | ||||
| ) | ||||
|  | ||||
| type transport struct { | ||||
| @@ -41,7 +45,32 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) | ||||
| 		return respNotFound(req, fmt.Sprintf("%s is directory", req.URL.Path)), nil | ||||
| 	} | ||||
|  | ||||
| 	if config.ETagEnabled { | ||||
| 	statusCode := 200 | ||||
| 	size := fi.Size() | ||||
| 	body := io.ReadCloser(f) | ||||
|  | ||||
| 	mime := mime.TypeByExtension(filepath.Ext(fi.Name())) | ||||
| 	header.Set("Content-Type", mime) | ||||
|  | ||||
| 	start, end, err := httprange.Parse(req.Header.Get("Range")) | ||||
| 	switch { | ||||
| 	case err != nil: | ||||
| 		f.Close() | ||||
| 		return httprange.InvalidHTTPRangeResponse(req), nil | ||||
|  | ||||
| 	case end != 0: | ||||
| 		if end < 0 { | ||||
| 			end = size - 1 | ||||
| 		} | ||||
|  | ||||
| 		f.Seek(start, io.SeekStart) | ||||
|  | ||||
| 		statusCode = http.StatusPartialContent | ||||
| 		size = end - start + 1 | ||||
| 		body = &fileLimiter{f: f, left: int(size)} | ||||
| 		header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fi.Size())) | ||||
|  | ||||
| 	case config.ETagEnabled: | ||||
| 		etag := BuildEtag(req.URL.Path, fi) | ||||
| 		header.Set("ETag", etag) | ||||
|  | ||||
| @@ -62,15 +91,16 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	header.Set("Content-Length", strconv.Itoa(int(size))) | ||||
|  | ||||
| 	return &http.Response{ | ||||
| 		Status:        "200 OK", | ||||
| 		StatusCode:    200, | ||||
| 		StatusCode:    statusCode, | ||||
| 		Proto:         "HTTP/1.0", | ||||
| 		ProtoMajor:    1, | ||||
| 		ProtoMinor:    0, | ||||
| 		Header:        header, | ||||
| 		ContentLength: fi.Size(), | ||||
| 		Body:          f, | ||||
| 		ContentLength: size, | ||||
| 		Body:          body, | ||||
| 		Close:         true, | ||||
| 		Request:       req, | ||||
| 	}, nil | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
| 	"google.golang.org/api/option" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/config" | ||||
| 	"github.com/imgproxy/imgproxy/v3/httprange" | ||||
| ) | ||||
|  | ||||
| // For tests | ||||
| @@ -58,40 +59,82 @@ func (t transport) RoundTrip(req *http.Request) (*http.Response, error) { | ||||
| 		obj = obj.Generation(g) | ||||
| 	} | ||||
|  | ||||
| 	var ( | ||||
| 		reader     *storage.Reader | ||||
| 		statusCode int | ||||
| 		size       int64 | ||||
| 	) | ||||
|  | ||||
| 	header := make(http.Header) | ||||
|  | ||||
| 	if config.ETagEnabled { | ||||
| 		attrs, err := obj.Attrs(req.Context()) | ||||
| 	if r := req.Header.Get("Range"); len(r) != 0 { | ||||
| 		start, end, err := httprange.Parse(r) | ||||
| 		if err != nil { | ||||
| 			return httprange.InvalidHTTPRangeResponse(req), nil | ||||
| 		} | ||||
|  | ||||
| 		if end != 0 { | ||||
| 			length := end - start + 1 | ||||
| 			if end < 0 { | ||||
| 				length = -1 | ||||
| 			} | ||||
|  | ||||
| 			reader, err = obj.NewRangeReader(req.Context(), start, length) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			if end < 0 || end >= reader.Attrs.Size { | ||||
| 				end = reader.Attrs.Size - 1 | ||||
| 			} | ||||
|  | ||||
| 			size = end - reader.Attrs.StartOffset + 1 | ||||
|  | ||||
| 			statusCode = http.StatusPartialContent | ||||
| 			header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", reader.Attrs.StartOffset, end, reader.Attrs.Size)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// We haven't initialize reader yet, this means that we need non-ranged reader | ||||
| 	if reader == nil { | ||||
| 		if config.ETagEnabled { | ||||
| 			attrs, err := obj.Attrs(req.Context()) | ||||
| 			if err != nil { | ||||
| 				return handleError(req, err) | ||||
| 			} | ||||
| 			header.Set("ETag", attrs.Etag) | ||||
|  | ||||
| 			if etag := req.Header.Get("If-None-Match"); len(etag) > 0 && attrs.Etag == etag { | ||||
| 				return &http.Response{ | ||||
| 					StatusCode:    http.StatusNotModified, | ||||
| 					Proto:         "HTTP/1.0", | ||||
| 					ProtoMajor:    1, | ||||
| 					ProtoMinor:    0, | ||||
| 					Header:        header, | ||||
| 					ContentLength: 0, | ||||
| 					Body:          nil, | ||||
| 					Close:         false, | ||||
| 					Request:       req, | ||||
| 				}, nil | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var err error | ||||
| 		reader, err = obj.NewReader(req.Context()) | ||||
| 		if err != nil { | ||||
| 			return handleError(req, err) | ||||
| 		} | ||||
| 		header.Set("ETag", attrs.Etag) | ||||
|  | ||||
| 		if etag := req.Header.Get("If-None-Match"); len(etag) > 0 && attrs.Etag == etag { | ||||
| 			return &http.Response{ | ||||
| 				StatusCode:    http.StatusNotModified, | ||||
| 				Proto:         "HTTP/1.0", | ||||
| 				ProtoMajor:    1, | ||||
| 				ProtoMinor:    0, | ||||
| 				Header:        header, | ||||
| 				ContentLength: 0, | ||||
| 				Body:          nil, | ||||
| 				Close:         false, | ||||
| 				Request:       req, | ||||
| 			}, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	reader, err := obj.NewReader(req.Context()) | ||||
| 	if err != nil { | ||||
| 		return handleError(req, err) | ||||
| 		statusCode = 200 | ||||
| 		size = reader.Attrs.Size | ||||
| 	} | ||||
|  | ||||
| 	header.Set("Content-Length", strconv.Itoa(int(size))) | ||||
| 	header.Set("Content-Type", reader.Attrs.ContentType) | ||||
| 	header.Set("Cache-Control", reader.Attrs.CacheControl) | ||||
|  | ||||
| 	return &http.Response{ | ||||
| 		Status:        "200 OK", | ||||
| 		StatusCode:    200, | ||||
| 		StatusCode:    statusCode, | ||||
| 		Proto:         "HTTP/1.0", | ||||
| 		ProtoMajor:    1, | ||||
| 		ProtoMinor:    0, | ||||
|   | ||||
| @@ -53,7 +53,9 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) | ||||
| 		input.VersionId = aws.String(req.URL.RawQuery) | ||||
| 	} | ||||
|  | ||||
| 	if config.ETagEnabled { | ||||
| 	if r := req.Header.Get("Range"); len(r) != 0 { | ||||
| 		input.Range = aws.String(r) | ||||
| 	} else if config.ETagEnabled { | ||||
| 		if ifNoneMatch := req.Header.Get("If-None-Match"); len(ifNoneMatch) > 0 { | ||||
| 			input.IfNoneMatch = aws.String(ifNoneMatch) | ||||
| 		} | ||||
|   | ||||
| @@ -46,7 +46,12 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error) | ||||
| 	container := req.URL.Host | ||||
| 	objectName := strings.TrimPrefix(req.URL.Path, "/") | ||||
|  | ||||
| 	object, objectHeaders, err := t.con.ObjectOpen(req.Context(), container, objectName, false, make(swift.Headers)) | ||||
| 	reqHeaders := make(swift.Headers) | ||||
| 	if r := req.Header.Get("Range"); len(r) > 0 { | ||||
| 		reqHeaders["Range"] = r | ||||
| 	} | ||||
|  | ||||
| 	object, objectHeaders, err := t.con.ObjectOpen(req.Context(), container, objectName, false, reqHeaders) | ||||
|  | ||||
| 	header := make(http.Header) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user