added range support to continue interrupted download

This commit is contained in:
Altair-sh
2026-11-06 20:57:37 +01:00
parent 2098082aa6
commit 0bfa5ed35c
6 changed files with 187 additions and 14 deletions
+70
View File
@@ -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
}
+54
View File
@@ -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)
}
}