Parse content ranges
This commit is contained in:
parent
3573790185
commit
836c5c69f0
|
@ -0,0 +1,74 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errInvalidRange = errors.New("invalid Content-Range")
|
||||||
|
|
||||||
|
type contentRange struct {
|
||||||
|
Start int64
|
||||||
|
End int64
|
||||||
|
Size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseContentRange parse a Content-Range string like "5-10/100".
|
||||||
|
// see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 .
|
||||||
|
// Asterisks "*" will result in End/Size being set to -1.
|
||||||
|
func parseContentRange(s string) (*contentRange, error) {
|
||||||
|
const prefix = "bytes "
|
||||||
|
offset := strings.Index(s, prefix)
|
||||||
|
if offset != 0 {
|
||||||
|
return nil, errInvalidRange
|
||||||
|
}
|
||||||
|
s = s[len(prefix):]
|
||||||
|
|
||||||
|
parts := strings.Split(s, "/")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, errInvalidRange
|
||||||
|
}
|
||||||
|
|
||||||
|
r := new(contentRange)
|
||||||
|
|
||||||
|
if parts[0] == "*" {
|
||||||
|
r.Start = 0
|
||||||
|
r.End = -1
|
||||||
|
} else {
|
||||||
|
offsets := strings.Split(parts[0], "-")
|
||||||
|
if len(offsets) != 2 {
|
||||||
|
return nil, errInvalidRange
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset, err := strconv.ParseInt(offsets[0], 10, 64); err == nil {
|
||||||
|
r.Start = offset
|
||||||
|
} else {
|
||||||
|
return nil, errInvalidRange
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset, err := strconv.ParseInt(offsets[1], 10, 64); err == nil {
|
||||||
|
r.End = offset
|
||||||
|
} else {
|
||||||
|
return nil, errInvalidRange
|
||||||
|
}
|
||||||
|
|
||||||
|
// A byte-content-range-spec with a byte-range-resp-spec whose last-
|
||||||
|
// byte-pos value is less than its first-byte-pos value, or whose
|
||||||
|
// instance-length value is less than or equal to its last-byte-pos value,
|
||||||
|
// is invalid. The recipient of an invalid byte-content-range- spec MUST
|
||||||
|
// ignore it and any content transferred along with it.
|
||||||
|
if r.End <= r.Start {
|
||||||
|
return nil, errInvalidRange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[1] == "*" {
|
||||||
|
r.Size = -1
|
||||||
|
return r, nil
|
||||||
|
} else if size, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
|
||||||
|
r.Size = size
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, errInvalidRange
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ContentRangeTests = []struct {
|
||||||
|
s string
|
||||||
|
want contentRange
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{s: "bytes 0-5/100", want: contentRange{Start: 0, End: 5, Size: 100}},
|
||||||
|
{s: "bytes 5-20/30", want: contentRange{Start: 5, End: 20, Size: 30}},
|
||||||
|
{s: "bytes */100", want: contentRange{Start: 0, End: -1, Size: 100}},
|
||||||
|
{s: "bytes 5-20/*", want: contentRange{Start: 5, End: 20, Size: -1}},
|
||||||
|
{s: "bytes */*", want: contentRange{Start: 0, End: -1, Size: -1}},
|
||||||
|
{s: "bytes 0-2147483647/2147483648", want: contentRange{Start: 0, End: 2147483647, Size: 2147483648}},
|
||||||
|
{s: "bytes 5-20", err: "invalid"},
|
||||||
|
{s: "bytes 5-5/100", err: "invalid"},
|
||||||
|
{s: "bytes 5-4/100", err: "invalid"},
|
||||||
|
{s: "bytes ", err: "invalid"},
|
||||||
|
{s: "", err: "invalid"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseContentRange(t *testing.T) {
|
||||||
|
for _, test := range ContentRangeTests {
|
||||||
|
t.Logf("testing: %s", test.s)
|
||||||
|
|
||||||
|
r, err := parseContentRange(test.s)
|
||||||
|
if test.err != "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("got no error, but expected: %s", test.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
errMatch := regexp.MustCompile(test.err)
|
||||||
|
if !errMatch.MatchString(err.Error()) {
|
||||||
|
t.Errorf("unexpected error: %s, wanted: %s", err, test.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s, wanted: %+v", err, test.want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if *r != test.want {
|
||||||
|
t.Errorf("got: %+v, wanted: %+v", r, test.want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,9 +31,14 @@ func reply(w http.ResponseWriter, code int, message string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(w http.ResponseWriter, r *http.Request) {
|
func createFile(w http.ResponseWriter, r *http.Request) {
|
||||||
contentRange := r.Header.Get("Content-Range")
|
contentRange, err := parseContentRange(r.Header.Get("Content-Range"))
|
||||||
if contentRange == "" {
|
if err != nil {
|
||||||
reply(w, http.StatusBadRequest, "Content-Range header is required")
|
reply(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentRange.End != -1 {
|
||||||
|
reply(w, http.StatusNotImplemented, "File data in initial request.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,4 +48,5 @@ func createFile(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("contentType: %s", contentType)
|
log.Printf("contentType: %s", contentType)
|
||||||
|
log.Printf("range: %#v", contentRange)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue