Skip to content

Commit 58fd025

Browse files
committed
Add ranged requests suport to transports
1 parent 0f7281e commit 58fd025

File tree

7 files changed

+222
-31
lines changed

7 files changed

+222
-31
lines changed

httprange/httprange.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package httprange
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/textproto"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
func Parse(s string) (int64, int64, error) {
12+
if s == "" {
13+
return 0, 0, nil // header not present
14+
}
15+
16+
const b = "bytes="
17+
if !strings.HasPrefix(s, b) {
18+
return 0, 0, errors.New("invalid range")
19+
}
20+
21+
for _, ra := range strings.Split(s[len(b):], ",") {
22+
ra = textproto.TrimString(ra)
23+
if ra == "" {
24+
continue
25+
}
26+
27+
i := strings.Index(ra, "-")
28+
if i < 0 {
29+
return 0, 0, errors.New("invalid range")
30+
}
31+
32+
start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])
33+
34+
if start == "" {
35+
// Don't support ranges without start since it looks like FFmpeg doen't use ones
36+
return 0, 0, errors.New("invalid range")
37+
}
38+
39+
istart, err := strconv.ParseInt(start, 10, 64)
40+
if err != nil || i < 0 {
41+
return 0, 0, errors.New("invalid range")
42+
}
43+
44+
var iend int64
45+
46+
if end == "" {
47+
iend = -1
48+
} else {
49+
iend, err = strconv.ParseInt(end, 10, 64)
50+
if err != nil || istart > iend {
51+
return 0, 0, errors.New("invalid range")
52+
}
53+
}
54+
55+
return istart, iend, nil
56+
}
57+
58+
return 0, 0, errors.New("invalid range")
59+
}
60+
61+
func InvalidHTTPRangeResponse(req *http.Request) *http.Response {
62+
return &http.Response{
63+
StatusCode: http.StatusRequestedRangeNotSatisfiable,
64+
Proto: "HTTP/1.0",
65+
ProtoMajor: 1,
66+
ProtoMinor: 0,
67+
Header: make(http.Header),
68+
ContentLength: 0,
69+
Body: nil,
70+
Close: false,
71+
Request: req,
72+
}
73+
}

transport/azure/azure.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/Azure/azure-storage-blob-go/azblob"
1010
"github.com/imgproxy/imgproxy/v3/config"
11+
"github.com/imgproxy/imgproxy/v3/httprange"
1112
)
1213

1314
type transport struct {
@@ -40,12 +41,22 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
4041
containerURL := t.serviceURL.NewContainerURL(strings.ToLower(req.URL.Host))
4142
blobURL := containerURL.NewBlockBlobURL(strings.TrimPrefix(req.URL.Path, "/"))
4243

43-
get, err := blobURL.Download(req.Context(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
44+
start, end, err := httprange.Parse(req.Header.Get("Range"))
45+
if err != nil {
46+
return httprange.InvalidHTTPRangeResponse(req), nil
47+
}
48+
49+
length := end - start + 1
50+
if end <= 0 {
51+
length = azblob.CountToEnd
52+
}
53+
54+
get, err := blobURL.Download(req.Context(), start, length, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
4455
if err != nil {
4556
return nil, err
4657
}
4758

48-
if config.ETagEnabled {
59+
if config.ETagEnabled && start == 0 && end == azblob.CountToEnd {
4960
etag := string(get.ETag())
5061

5162
if etag == req.Header.Get("If-None-Match") {

transport/fs/file_limiter.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package fs
2+
3+
import (
4+
"io"
5+
"net/http"
6+
)
7+
8+
type fileLimiter struct {
9+
f http.File
10+
left int
11+
}
12+
13+
func (lr *fileLimiter) Read(p []byte) (n int, err error) {
14+
if lr.left <= 0 {
15+
return 0, io.EOF
16+
}
17+
if len(p) > lr.left {
18+
p = p[0:lr.left]
19+
}
20+
n, err = lr.f.Read(p)
21+
lr.left -= n
22+
return
23+
}
24+
25+
func (lr *fileLimiter) Close() error {
26+
return lr.f.Close()
27+
}

transport/fs/fs.go

+35-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import (
66
"fmt"
77
"io"
88
"io/fs"
9+
"mime"
910
"net/http"
1011
"os"
12+
"path/filepath"
13+
"strconv"
1114
"strings"
1215

1316
"github.com/imgproxy/imgproxy/v3/config"
17+
"github.com/imgproxy/imgproxy/v3/httprange"
1418
)
1519

1620
type transport struct {
@@ -41,7 +45,32 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
4145
return respNotFound(req, fmt.Sprintf("%s is directory", req.URL.Path)), nil
4246
}
4347

44-
if config.ETagEnabled {
48+
statusCode := 200
49+
size := fi.Size()
50+
body := io.ReadCloser(f)
51+
52+
mime := mime.TypeByExtension(filepath.Ext(fi.Name()))
53+
header.Set("Content-Type", mime)
54+
55+
start, end, err := httprange.Parse(req.Header.Get("Range"))
56+
switch {
57+
case err != nil:
58+
f.Close()
59+
return httprange.InvalidHTTPRangeResponse(req), nil
60+
61+
case end != 0:
62+
if end < 0 {
63+
end = size - 1
64+
}
65+
66+
f.Seek(start, io.SeekStart)
67+
68+
statusCode = http.StatusPartialContent
69+
size = end - start + 1
70+
body = &fileLimiter{f: f, left: int(size)}
71+
header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fi.Size()))
72+
73+
case config.ETagEnabled:
4574
etag := BuildEtag(req.URL.Path, fi)
4675
header.Set("ETag", etag)
4776

@@ -62,15 +91,16 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
6291
}
6392
}
6493

94+
header.Set("Content-Length", strconv.Itoa(int(size)))
95+
6596
return &http.Response{
66-
Status: "200 OK",
67-
StatusCode: 200,
97+
StatusCode: statusCode,
6898
Proto: "HTTP/1.0",
6999
ProtoMajor: 1,
70100
ProtoMinor: 0,
71101
Header: header,
72-
ContentLength: fi.Size(),
73-
Body: f,
102+
ContentLength: size,
103+
Body: body,
74104
Close: true,
75105
Request: req,
76106
}, nil

transport/gcs/gcs.go

+65-22
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"google.golang.org/api/option"
1313

1414
"github.com/imgproxy/imgproxy/v3/config"
15+
"github.com/imgproxy/imgproxy/v3/httprange"
1516
)
1617

1718
// For tests
@@ -58,40 +59,82 @@ func (t transport) RoundTrip(req *http.Request) (*http.Response, error) {
5859
obj = obj.Generation(g)
5960
}
6061

62+
var (
63+
reader *storage.Reader
64+
statusCode int
65+
size int64
66+
)
67+
6168
header := make(http.Header)
6269

63-
if config.ETagEnabled {
64-
attrs, err := obj.Attrs(req.Context())
70+
if r := req.Header.Get("Range"); len(r) != 0 {
71+
start, end, err := httprange.Parse(r)
6572
if err != nil {
66-
return handleError(req, err)
73+
return httprange.InvalidHTTPRangeResponse(req), nil
6774
}
68-
header.Set("ETag", attrs.Etag)
69-
70-
if etag := req.Header.Get("If-None-Match"); len(etag) > 0 && attrs.Etag == etag {
71-
return &http.Response{
72-
StatusCode: http.StatusNotModified,
73-
Proto: "HTTP/1.0",
74-
ProtoMajor: 1,
75-
ProtoMinor: 0,
76-
Header: header,
77-
ContentLength: 0,
78-
Body: nil,
79-
Close: false,
80-
Request: req,
81-
}, nil
75+
76+
if end != 0 {
77+
length := end - start + 1
78+
if end < 0 {
79+
length = -1
80+
}
81+
82+
reader, err = obj.NewRangeReader(req.Context(), start, length)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
if end < 0 || end >= reader.Attrs.Size {
88+
end = reader.Attrs.Size - 1
89+
}
90+
91+
size = end - reader.Attrs.StartOffset + 1
92+
93+
statusCode = http.StatusPartialContent
94+
header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", reader.Attrs.StartOffset, end, reader.Attrs.Size))
8295
}
8396
}
8497

85-
reader, err := obj.NewReader(req.Context())
86-
if err != nil {
87-
return handleError(req, err)
98+
// We haven't initialize reader yet, this means that we need non-ranged reader
99+
if reader == nil {
100+
if config.ETagEnabled {
101+
attrs, err := obj.Attrs(req.Context())
102+
if err != nil {
103+
return handleError(req, err)
104+
}
105+
header.Set("ETag", attrs.Etag)
106+
107+
if etag := req.Header.Get("If-None-Match"); len(etag) > 0 && attrs.Etag == etag {
108+
return &http.Response{
109+
StatusCode: http.StatusNotModified,
110+
Proto: "HTTP/1.0",
111+
ProtoMajor: 1,
112+
ProtoMinor: 0,
113+
Header: header,
114+
ContentLength: 0,
115+
Body: nil,
116+
Close: false,
117+
Request: req,
118+
}, nil
119+
}
120+
}
121+
122+
var err error
123+
reader, err = obj.NewReader(req.Context())
124+
if err != nil {
125+
return handleError(req, err)
126+
}
127+
128+
statusCode = 200
129+
size = reader.Attrs.Size
88130
}
89131

132+
header.Set("Content-Length", strconv.Itoa(int(size)))
133+
header.Set("Content-Type", reader.Attrs.ContentType)
90134
header.Set("Cache-Control", reader.Attrs.CacheControl)
91135

92136
return &http.Response{
93-
Status: "200 OK",
94-
StatusCode: 200,
137+
StatusCode: statusCode,
95138
Proto: "HTTP/1.0",
96139
ProtoMajor: 1,
97140
ProtoMinor: 0,

transport/s3/s3.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
5353
input.VersionId = aws.String(req.URL.RawQuery)
5454
}
5555

56-
if config.ETagEnabled {
56+
if r := req.Header.Get("Range"); len(r) != 0 {
57+
input.Range = aws.String(r)
58+
} else if config.ETagEnabled {
5759
if ifNoneMatch := req.Header.Get("If-None-Match"); len(ifNoneMatch) > 0 {
5860
input.IfNoneMatch = aws.String(ifNoneMatch)
5961
}

transport/swift/swift.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ func (t transport) RoundTrip(req *http.Request) (resp *http.Response, err error)
4646
container := req.URL.Host
4747
objectName := strings.TrimPrefix(req.URL.Path, "/")
4848

49-
object, objectHeaders, err := t.con.ObjectOpen(req.Context(), container, objectName, false, make(swift.Headers))
49+
reqHeaders := make(swift.Headers)
50+
if r := req.Header.Get("Range"); len(r) > 0 {
51+
reqHeaders["Range"] = r
52+
}
53+
54+
object, objectHeaders, err := t.con.ObjectOpen(req.Context(), container, objectName, false, reqHeaders)
5055

5156
header := make(http.Header)
5257

0 commit comments

Comments
 (0)