added range support to continue interrupted download
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RangeResult int
|
||||
|
||||
const (
|
||||
// no header, invalid syntax or multiple ranges: serve the full body with 200
|
||||
RangeNone RangeResult = iota
|
||||
// serve bytes [start, end] (inclusive) with 206
|
||||
RangeOK
|
||||
// respond with 416
|
||||
RangeUnsatisfiable
|
||||
)
|
||||
|
||||
// ParseRange parses a "Range" header value for a resource of the given size.
|
||||
// Only a single byte range is supported: "bytes=a-b", "bytes=a-", "bytes=-n".
|
||||
// Anything else is ignored (RangeNone), which RFC 9110 §14.2 allows.
|
||||
func ParseRange(header string, size int64) (start, end int64, r RangeResult) {
|
||||
header = strings.TrimSpace(header)
|
||||
if header == "" {
|
||||
return 0, 0, RangeNone
|
||||
}
|
||||
spec, ok := strings.CutPrefix(header, "bytes=")
|
||||
if !ok || strings.ContainsRune(spec, ',') {
|
||||
return 0, 0, RangeNone
|
||||
}
|
||||
firstStr, lastStr, ok := strings.Cut(spec, "-")
|
||||
if !ok {
|
||||
return 0, 0, RangeNone
|
||||
}
|
||||
firstStr = strings.TrimSpace(firstStr)
|
||||
lastStr = strings.TrimSpace(lastStr)
|
||||
|
||||
if firstStr == "" {
|
||||
// suffix range: last n bytes
|
||||
n, ok := parseNonNegative(lastStr)
|
||||
if !ok {
|
||||
return 0, 0, RangeNone
|
||||
}
|
||||
if n == 0 || size == 0 {
|
||||
return 0, 0, RangeUnsatisfiable
|
||||
}
|
||||
return max(size-n, 0), size - 1, RangeOK
|
||||
}
|
||||
|
||||
first, ok := parseNonNegative(firstStr)
|
||||
if !ok {
|
||||
return 0, 0, RangeNone
|
||||
}
|
||||
last := size - 1
|
||||
if lastStr != "" {
|
||||
last, ok = parseNonNegative(lastStr)
|
||||
if !ok || last < first {
|
||||
return 0, 0, RangeNone
|
||||
}
|
||||
}
|
||||
if first >= size {
|
||||
return 0, 0, RangeUnsatisfiable
|
||||
}
|
||||
return first, min(last, size-1), RangeOK
|
||||
}
|
||||
|
||||
func parseNonNegative(s string) (int64, bool) {
|
||||
n, err := strconv.ParseUint(s, 10, 63)
|
||||
return int64(n), err == nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package http
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseRange(t *testing.T) {
|
||||
const size = 1000
|
||||
tests := []struct {
|
||||
header string
|
||||
start, end int64
|
||||
result RangeResult
|
||||
}{
|
||||
{"", 0, 0, RangeNone},
|
||||
{"bytes=0-", 0, 999, RangeOK},
|
||||
{"bytes=500-", 500, 999, RangeOK},
|
||||
{"bytes=0-499", 0, 499, RangeOK},
|
||||
{"bytes=999-999", 999, 999, RangeOK},
|
||||
{"bytes=-100", 900, 999, RangeOK},
|
||||
{"bytes=-5000", 0, 999, RangeOK},
|
||||
{"bytes=900-5000", 900, 999, RangeOK},
|
||||
{" bytes= 10 - 20 ", 10, 20, RangeOK},
|
||||
{"bytes=1000-", 0, 0, RangeUnsatisfiable},
|
||||
{"bytes=1000-2000", 0, 0, RangeUnsatisfiable},
|
||||
{"bytes=-0", 0, 0, RangeUnsatisfiable},
|
||||
{"bytes=5-1", 0, 0, RangeNone},
|
||||
{"bytes=0-1,5-9", 0, 0, RangeNone},
|
||||
{"items=0-1", 0, 0, RangeNone},
|
||||
{"bytes=abc-", 0, 0, RangeNone},
|
||||
{"bytes=+5-", 0, 0, RangeNone},
|
||||
{"bytes=--5", 0, 0, RangeNone},
|
||||
{"bytes=5", 0, 0, RangeNone},
|
||||
{"bytes=-", 0, 0, RangeNone},
|
||||
{"bytes=99999999999999999999-", 0, 0, RangeNone},
|
||||
{"bytes=9223372036854775807-", 0, 0, RangeUnsatisfiable}, // MaxInt64
|
||||
{"bytes=9223372036854775808-", 0, 0, RangeNone}, // MaxInt64+1
|
||||
{"bytes=0-9223372036854775808", 0, 0, RangeNone},
|
||||
{"bytes=1_0-", 0, 0, RangeNone},
|
||||
{"bytes=-+5", 0, 0, RangeNone},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
start, end, r := ParseRange(tt.header, size)
|
||||
if r != tt.result || start != tt.start || end != tt.end {
|
||||
t.Errorf("ParseRange(%q) = (%d, %d, %d), want (%d, %d, %d)",
|
||||
tt.header, start, end, r, tt.start, tt.end, tt.result)
|
||||
}
|
||||
}
|
||||
|
||||
// empty file: any range is unsatisfiable
|
||||
if _, _, r := ParseRange("bytes=0-", 0); r != RangeUnsatisfiable {
|
||||
t.Errorf("empty file bytes=0-: got %d, want RangeUnsatisfiable", r)
|
||||
}
|
||||
if _, _, r := ParseRange("bytes=-10", 0); r != RangeUnsatisfiable {
|
||||
t.Errorf("empty file bytes=-10: got %d, want RangeUnsatisfiable", r)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user