|
|
|
package http_range |
|
|
|
import ( |
|
"errors" |
|
"fmt" |
|
"net/http" |
|
"net/textproto" |
|
"strconv" |
|
"strings" |
|
) |
|
|
|
|
|
type Range struct { |
|
Start int64 |
|
Length int64 |
|
} |
|
|
|
|
|
func (r Range) ContentRange(size int64) string { |
|
return fmt.Sprintf("bytes %d-%d/%d", r.Start, r.Start+r.Length-1, size) |
|
} |
|
|
|
var ( |
|
|
|
|
|
ErrNoOverlap = errors.New("invalid range: failed to overlap") |
|
|
|
|
|
ErrInvalid = errors.New("invalid range") |
|
) |
|
|
|
|
|
|
|
|
|
func ParseRange(s string, size int64) ([]Range, error) { |
|
if s == "" { |
|
return nil, nil |
|
} |
|
const b = "bytes=" |
|
if !strings.HasPrefix(s, b) { |
|
return nil, ErrInvalid |
|
} |
|
var ranges []Range |
|
noOverlap := false |
|
for _, ra := range strings.Split(s[len(b):], ",") { |
|
ra = textproto.TrimString(ra) |
|
if ra == "" { |
|
continue |
|
} |
|
i := strings.Index(ra, "-") |
|
if i < 0 { |
|
return nil, ErrInvalid |
|
} |
|
start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:]) |
|
var r Range |
|
if start == "" { |
|
|
|
|
|
|
|
|
|
|
|
if end == "" || end[0] == '-' { |
|
return nil, ErrInvalid |
|
} |
|
i, err := strconv.ParseInt(end, 10, 64) |
|
if i < 0 || err != nil { |
|
return nil, ErrInvalid |
|
} |
|
if i > size { |
|
i = size |
|
} |
|
r.Start = size - i |
|
r.Length = size - r.Start |
|
} else { |
|
i, err := strconv.ParseInt(start, 10, 64) |
|
if err != nil || i < 0 { |
|
return nil, ErrInvalid |
|
} |
|
if i >= size { |
|
|
|
|
|
noOverlap = true |
|
continue |
|
} |
|
r.Start = i |
|
if end == "" { |
|
|
|
r.Length = size - r.Start |
|
} else { |
|
i, err := strconv.ParseInt(end, 10, 64) |
|
if err != nil || r.Start > i { |
|
return nil, ErrInvalid |
|
} |
|
if i >= size { |
|
i = size - 1 |
|
} |
|
r.Length = i - r.Start + 1 |
|
} |
|
} |
|
ranges = append(ranges, r) |
|
} |
|
if noOverlap && len(ranges) == 0 { |
|
|
|
return nil, ErrNoOverlap |
|
} |
|
return ranges, nil |
|
} |
|
|
|
|
|
func ParseContentRange(s string) (start, end int64, err error) { |
|
if s == "" { |
|
return 0, 0, ErrInvalid |
|
} |
|
const b = "bytes " |
|
if !strings.HasPrefix(s, b) { |
|
return 0, 0, ErrInvalid |
|
} |
|
p1 := strings.Index(s, "-") |
|
p2 := strings.Index(s, "/") |
|
if p1 < 0 || p2 < 0 { |
|
return 0, 0, ErrInvalid |
|
} |
|
startStr, endStr := textproto.TrimString(s[len(b):p1]), textproto.TrimString(s[p1+1:p2]) |
|
start, startErr := strconv.ParseInt(startStr, 10, 64) |
|
end, endErr := strconv.ParseInt(endStr, 10, 64) |
|
|
|
return start, end, errors.Join(startErr, endErr) |
|
} |
|
|
|
func (r Range) MimeHeader(contentType string, size int64) textproto.MIMEHeader { |
|
return textproto.MIMEHeader{ |
|
"Content-Range": {r.ContentRange(size)}, |
|
"Content-Type": {contentType}, |
|
} |
|
} |
|
|
|
|
|
func ApplyRangeToHttpHeader(p Range, headerRef http.Header) http.Header { |
|
header := headerRef |
|
if header == nil { |
|
header = http.Header{} |
|
} |
|
if p.Start == 0 && p.Length < 0 { |
|
header.Del("Range") |
|
} else { |
|
end := "" |
|
if p.Length >= 0 { |
|
end = strconv.FormatInt(p.Start+p.Length-1, 10) |
|
} |
|
header.Set("Range", fmt.Sprintf("bytes=%v-%v", p.Start, end)) |
|
} |
|
return header |
|
} |
|
|