package httputil // Original source: https://github.com/jfbus/httprs/tree/master /* Package httprs provides a ReadSeeker for http.Response.Body. Usage : resp, err := http.Get(url) rs := httprs.NewHttpReadSeeker(resp) defer rs.Close() io.ReadFull(rs, buf) // reads the first bytes from the response body rs.Seek(1024, 0) // moves the position, but does no range request io.ReadFull(rs, buf) // does a range request and reads from the response body If you want to use a specific http.Client for additional range requests : rs := httprs.NewHttpReadSeeker(resp, client) */ import ( "fmt" "io" "net/http" "seanime/internal/util/limiter" "strconv" "strings" "sync" ) // HttpReadSeeker implements io.ReadSeeker for HTTP responses // It allows seeking within an HTTP response by using HTTP Range requests type HttpReadSeeker struct { url string // The URL of the resource client *http.Client // HTTP client to use for requests resp *http.Response // Current response offset int64 // Current offset in the resource size int64 // Size of the resource, -1 if unknown readBuf []byte // Buffer for reading readOffset int // Current offset in readBuf mu sync.Mutex // Mutex for thread safety rateLimiter *limiter.Limiter } // NewHttpReadSeeker creates a new HttpReadSeeker from an http.Response func NewHttpReadSeeker(resp *http.Response) *HttpReadSeeker { url := "" if resp.Request != nil { url = resp.Request.URL.String() } size := int64(-1) if resp.ContentLength > 0 { size = resp.ContentLength } return &HttpReadSeeker{ url: url, client: http.DefaultClient, resp: resp, offset: 0, size: size, readBuf: nil, readOffset: 0, } } func NewHttpReadSeekerFromURL(url string) (*HttpReadSeeker, error) { resp, err := http.Get(url) if err != nil { return nil, fmt.Errorf("httprs: failed to get URL %s: %w", url, err) } return NewHttpReadSeeker(resp), nil } // Read implements io.Reader func (hrs *HttpReadSeeker) Read(p []byte) (n int, err error) { hrs.mu.Lock() defer hrs.mu.Unlock() // If we have buffered data, read from it first if hrs.readBuf != nil && hrs.readOffset < len(hrs.readBuf) { n = copy(p, hrs.readBuf[hrs.readOffset:]) hrs.readOffset += n hrs.offset += int64(n) // Clear buffer if we've read it all if hrs.readOffset >= len(hrs.readBuf) { hrs.readBuf = nil hrs.readOffset = 0 } return n, nil } // If we don't have a response or it's been closed, get a new one if hrs.resp == nil { if err := hrs.makeRangeRequest(); err != nil { return 0, err } } // Read from the response body n, err = hrs.resp.Body.Read(p) hrs.offset += int64(n) return n, err } // Seek implements io.Seeker func (hrs *HttpReadSeeker) Seek(offset int64, whence int) (int64, error) { hrs.mu.Lock() defer hrs.mu.Unlock() var newOffset int64 switch whence { case io.SeekStart: newOffset = offset case io.SeekCurrent: newOffset = hrs.offset + offset case io.SeekEnd: if hrs.size < 0 { // If we don't know the size, we need to determine it if err := hrs.determineSize(); err != nil { return hrs.offset, err } } newOffset = hrs.size + offset default: return hrs.offset, fmt.Errorf("httprs: invalid whence %d", whence) } if newOffset < 0 { return hrs.offset, fmt.Errorf("httprs: negative position") } // If we're just moving the offset without reading, we can skip the request // We'll make a new request when Read is called if hrs.resp != nil { hrs.resp.Body.Close() hrs.resp = nil } hrs.offset = newOffset hrs.readBuf = nil hrs.readOffset = 0 return hrs.offset, nil } // Close closes the underlying response body func (hrs *HttpReadSeeker) Close() error { hrs.mu.Lock() defer hrs.mu.Unlock() if hrs.resp != nil { err := hrs.resp.Body.Close() hrs.resp = nil return err } return nil } // makeRangeRequest makes a new HTTP request with the Range header func (hrs *HttpReadSeeker) makeRangeRequest() error { req, err := http.NewRequest("GET", hrs.url, nil) if err != nil { return err } // Set Range header from current offset req.Header.Set("Range", fmt.Sprintf("bytes=%d-", hrs.offset)) // Make the request resp, err := hrs.client.Do(req) if err != nil { return err } // Check if the server supports range requests if resp.StatusCode != http.StatusPartialContent && hrs.offset > 0 { resp.Body.Close() return fmt.Errorf("httprs: server does not support range requests") } // Update our response and offset if hrs.resp != nil { hrs.resp.Body.Close() } hrs.resp = resp // Update the size if we get it from Content-Range if contentRange := resp.Header.Get("Content-Range"); contentRange != "" { // Format: bytes -/ parts := strings.Split(contentRange, "/") if len(parts) > 1 && parts[1] != "*" { if size, err := strconv.ParseInt(parts[1], 10, 64); err == nil { hrs.size = size } } } else if resp.ContentLength > 0 { // If we don't have a Content-Range header but we do have Content-Length, // then the size is the current offset plus the content length hrs.size = hrs.offset + resp.ContentLength } return nil } // determineSize makes a HEAD request to determine the size of the resource func (hrs *HttpReadSeeker) determineSize() error { req, err := http.NewRequest("HEAD", hrs.url, nil) if err != nil { return err } resp, err := hrs.client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.ContentLength > 0 { hrs.size = resp.ContentLength } else { // If we still don't know the size, return an error return fmt.Errorf("httprs: unable to determine resource size") } return nil } // ReadAt implements io.ReaderAt func (hrs *HttpReadSeeker) ReadAt(p []byte, off int64) (n int, err error) { // Save current offset currentOffset := hrs.offset // Seek to the requested offset if _, err := hrs.Seek(off, io.SeekStart); err != nil { return 0, err } // Read the data n, err = hrs.Read(p) // Restore the original offset if _, seekErr := hrs.Seek(currentOffset, io.SeekStart); seekErr != nil { // If we can't restore the offset, return that error instead if err == nil { err = seekErr } } return n, err } // Size returns the size of the resource, or -1 if unknown func (hrs *HttpReadSeeker) Size() int64 { hrs.mu.Lock() defer hrs.mu.Unlock() if hrs.size < 0 { // Try to determine the size _ = hrs.determineSize() } return hrs.size } // WithClient returns a new HttpReadSeeker with the specified client func (hrs *HttpReadSeeker) WithClient(client *http.Client) *HttpReadSeeker { hrs.mu.Lock() defer hrs.mu.Unlock() hrs.client = client return hrs } func (hrs *HttpReadSeeker) WithRateLimiter(rl *limiter.Limiter) *HttpReadSeeker { hrs.mu.Lock() defer hrs.mu.Unlock() hrs.rateLimiter = rl return hrs }