MeteorLight/http.go
DataHoarder 6b3aaf7d0b
All checks were successful
continuous-integration/drone/push Build is passing
Implemented song_fetch_url
2022-05-21 16:02:03 +02:00

199 lines
3.9 KiB
Go

package main
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"runtime"
"strconv"
)
const rangeReaderBufferSize = 4096
type RangeReadSeekCloser struct {
uri *url.URL
size int64
i int64
body io.ReadCloser
buf []byte
ib int64
closed bool
}
func NewRangeReadSeekCloser(uri string) (*RangeReadSeekCloser, error) {
parsedUrl, err := url.Parse(uri)
if err != nil {
return nil, err
}
r := &RangeReadSeekCloser{
uri: parsedUrl,
buf: make([]byte, 0, rangeReaderBufferSize),
}
if err = r.getInformation(); err != nil {
return nil, err
}
return r, nil
}
func (r *RangeReadSeekCloser) GetURI() string {
return r.uri.String()
}
func (r *RangeReadSeekCloser) retryConnect() error {
if r.closed {
return errors.New("already closed")
}
headers := make(http.Header)
startOffset := r.i
expectedLength := r.size - startOffset
headers.Set("Range", fmt.Sprintf("bytes=%d-", startOffset))
response, err := http.DefaultClient.Do(&http.Request{
Method: "GET",
URL: r.uri,
Header: headers,
})
if err != nil {
return err
}
r.body = response.Body
runtime.SetFinalizer(r.body, io.ReadCloser.Close)
if response.StatusCode != http.StatusPartialContent {
r.body.Close()
r.body = nil
return fmt.Errorf("response status code %d != %d", response.StatusCode, http.StatusPartialContent)
}
contentLength, err := strconv.ParseInt(response.Header.Get("content-length"), 10, 0)
if err != nil {
r.body.Close()
r.body = nil
return errors.New("server response does not have a valid Content-Length")
}
if contentLength != expectedLength {
r.body.Close()
r.body = nil
return fmt.Errorf("server returned %d bytes, expected %d", contentLength, expectedLength)
}
return nil
}
func (r *RangeReadSeekCloser) Read(p []byte) (n int, err error) {
for retry := 0; retry < 2; retry++ {
if r.i >= r.size {
return 0, io.EOF
}
if r.i >= r.ib {
bufStart := int(r.i - r.ib)
bufEnd := bufStart + len(p)
if bufEnd <= len(r.buf) {
copy(p, r.buf[bufStart:bufEnd])
r.i += int64(len(p))
return len(p), nil
}
}
if r.body == nil {
if err = r.retryConnect(); err != nil {
continue
}
}
data := make([]byte, rangeReaderBufferSize)
n, err = r.body.Read(data)
if err != nil { //detect actual eof, not a disconnection
if err == io.EOF || err == io.ErrUnexpectedEOF {
if r.i+int64(n) != r.size {
continue
}
} else {
continue
}
}
r.buf = data[:n]
r.ib = r.i
readBytes := len(p)
if n < readBytes {
readBytes = n
}
copy(p[:readBytes], data[:readBytes])
r.i += int64(readBytes)
return readBytes, nil
}
return 0, errors.New("could not retry")
}
func (r *RangeReadSeekCloser) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
r.i = offset
case io.SeekCurrent:
r.i += offset
case io.SeekEnd:
//todo: maybe without -1?
r.i = (r.size - 1) - offset
default:
return 0, fmt.Errorf("unknown whence %d", whence)
}
if r.i >= r.size {
return r.i, io.EOF
} else if r.i < 0 {
return r.i, io.ErrUnexpectedEOF
}
if r.i < r.ib || r.i >= r.ib+int64(len(r.buf)) {
if r.body != nil {
r.body.Close()
}
r.body = nil
}
return r.i, nil
}
func (r *RangeReadSeekCloser) Close() error {
if r.body != nil {
r.closed = true
defer func() {
r.body = nil
}()
return r.body.Close()
}
return nil
}
func (r *RangeReadSeekCloser) getInformation() error {
response, err := http.DefaultClient.Head(r.GetURI())
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("response status code %d != %d", response.StatusCode, http.StatusOK)
}
if response.Header.Get("accept-ranges") != "bytes" {
return errors.New("server does not accept Range")
}
if r.size, err = strconv.ParseInt(response.Header.Get("content-length"), 10, 0); err != nil {
return errors.New("server response does not have a valid Content-Length")
}
return nil
}