mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-09-28 12:37:47 +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/Azure/azure-storage-blob-go/azblob"
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/httprange"
|
||||||
)
|
)
|
||||||
|
|
||||||
type transport struct {
|
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))
|
containerURL := t.serviceURL.NewContainerURL(strings.ToLower(req.URL.Host))
|
||||||
blobURL := containerURL.NewBlockBlobURL(strings.TrimPrefix(req.URL.Path, "/"))
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.ETagEnabled {
|
if config.ETagEnabled && start == 0 && end == azblob.CountToEnd {
|
||||||
etag := string(get.ETag())
|
etag := string(get.ETag())
|
||||||
|
|
||||||
if etag == req.Header.Get("If-None-Match") {
|
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"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/httprange"
|
||||||
)
|
)
|
||||||
|
|
||||||
type transport struct {
|
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
|
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)
|
etag := BuildEtag(req.URL.Path, fi)
|
||||||
header.Set("ETag", etag)
|
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{
|
return &http.Response{
|
||||||
Status: "200 OK",
|
StatusCode: statusCode,
|
||||||
StatusCode: 200,
|
|
||||||
Proto: "HTTP/1.0",
|
Proto: "HTTP/1.0",
|
||||||
ProtoMajor: 1,
|
ProtoMajor: 1,
|
||||||
ProtoMinor: 0,
|
ProtoMinor: 0,
|
||||||
Header: header,
|
Header: header,
|
||||||
ContentLength: fi.Size(),
|
ContentLength: size,
|
||||||
Body: f,
|
Body: body,
|
||||||
Close: true,
|
Close: true,
|
||||||
Request: req,
|
Request: req,
|
||||||
}, nil
|
}, nil
|
||||||
|
@@ -12,6 +12,7 @@ import (
|
|||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/httprange"
|
||||||
)
|
)
|
||||||
|
|
||||||
// For tests
|
// For tests
|
||||||
@@ -58,40 +59,82 @@ func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
obj = obj.Generation(g)
|
obj = obj.Generation(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
reader *storage.Reader
|
||||||
|
statusCode int
|
||||||
|
size int64
|
||||||
|
)
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
|
|
||||||
if config.ETagEnabled {
|
if r := req.Header.Get("Range"); len(r) != 0 {
|
||||||
attrs, err := obj.Attrs(req.Context())
|
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 {
|
if err != nil {
|
||||||
return handleError(req, err)
|
return handleError(req, err)
|
||||||
}
|
}
|
||||||
header.Set("ETag", attrs.Etag)
|
|
||||||
|
|
||||||
if etag := req.Header.Get("If-None-Match"); len(etag) > 0 && attrs.Etag == etag {
|
statusCode = 200
|
||||||
return &http.Response{
|
size = reader.Attrs.Size
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header.Set("Content-Length", strconv.Itoa(int(size)))
|
||||||
|
header.Set("Content-Type", reader.Attrs.ContentType)
|
||||||
header.Set("Cache-Control", reader.Attrs.CacheControl)
|
header.Set("Cache-Control", reader.Attrs.CacheControl)
|
||||||
|
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
Status: "200 OK",
|
StatusCode: statusCode,
|
||||||
StatusCode: 200,
|
|
||||||
Proto: "HTTP/1.0",
|
Proto: "HTTP/1.0",
|
||||||
ProtoMajor: 1,
|
ProtoMajor: 1,
|
||||||
ProtoMinor: 0,
|
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)
|
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 {
|
if ifNoneMatch := req.Header.Get("If-None-Match"); len(ifNoneMatch) > 0 {
|
||||||
input.IfNoneMatch = aws.String(ifNoneMatch)
|
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
|
container := req.URL.Host
|
||||||
objectName := strings.TrimPrefix(req.URL.Path, "/")
|
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)
|
header := make(http.Header)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user