Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions v3/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,13 @@ func (c *Client) readResponse(resp *Response) (stateFunc, error) {
panic("grab: developer error: Response.HTTPResponse is nil")
}

if resp.requestMethod() == "GET" && resp.DidResume {
if resp.HTTPResponse.StatusCode != http.StatusPartialContent {
resp.DidResume = false
resp.bytesResumed = 0
}
}

// check expected size
resp.sizeUnsafe = resp.HTTPResponse.ContentLength
if resp.sizeUnsafe >= 0 {
Expand Down
59 changes: 59 additions & 0 deletions v3/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,65 @@ func TestAutoResume(t *testing.T) {
// TODO: test when existing file is corrupted
}

func TestResumeFallsBackToFullWhenServerReturns200OK(t *testing.T) {
filename := filepath.Join(t.TempDir(), "testResume200OK")
partial := make([]byte, 100)
for i := range partial {
partial[i] = 0xAA
}
if err := os.WriteFile(filename, partial, 0666); err != nil {
t.Fatal(err)
}

size := 512
fullBytes := make([]byte, size)
for i := 0; i < size; i++ {
fullBytes[i] = byte(i)
}

s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
if r.Method == "HEAD" {
w.WriteHeader(http.StatusOK)
return
}
// Server returns 200 OK with the full content, ignoring any Range header
w.WriteHeader(http.StatusOK)
w.Write(fullBytes)
}))
defer s.Close()

req := mustNewRequest(filename, s.URL)
h := sha256.New()
h.Write(fullBytes)
req.SetChecksum(sha256.New(), h.Sum(nil), true)

resp := mustDo(req)
testComplete(t, resp)

if resp.DidResume {
t.Errorf("expected Response.DidResume to be false when server returned 200 OK")
}
if resp.BytesComplete() != int64(size) {
t.Errorf("expected BytesComplete %d, got %d", size, resp.BytesComplete())
}
fi, err := os.Stat(filename)
if err != nil {
t.Fatal(err)
}
if fi.Size() != int64(size) {
t.Errorf("expected file size %d, got %d (file corrupted or appended)", size, fi.Size())
}
data, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, fullBytes) {
t.Errorf("downloaded file contents do not match expected full file")
}
}

func TestSkipExisting(t *testing.T) {
filename := filepath.Join(t.TempDir(), "testSkipExisting")

Expand Down
10 changes: 9 additions & 1 deletion v3/pkg/grabtest/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

// send header and status code
w.WriteHeader(h.statusCodeFunc(r))
code := h.statusCodeFunc(r)
if code == http.StatusOK && h.acceptRanges && r.Header.Get("Range") != "" {
code = http.StatusPartialContent
w.Header().Set(
"Content-Range",
fmt.Sprintf("bytes %d-%d/%d", offset, h.contentLength-1, h.contentLength),
)
}
w.WriteHeader(code)

// send body
if r.Method == "GET" {
Expand Down
12 changes: 9 additions & 3 deletions v3/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ func (c *Response) BytesComplete() int64 {
// complete, the average bytes/sec for the life of the download is returned.
func (c *Response) BytesPerSecond() float64 {
if c.IsComplete() {
return float64(c.transfer.N()) / c.Duration().Seconds()
if secs := c.Duration().Seconds(); secs > 0 {
return float64(c.transfer.N()) / secs
}
return 0
}
return c.transfer.BPS()
}
Expand All @@ -164,6 +167,9 @@ func (c *Response) Progress() float64 {
// the transfer is complete, the duration will be between the start and end of
// the completed transfer process.
func (c *Response) Duration() time.Duration {
if c.Start.IsZero() {
return 0
}
if c.IsComplete() {
return c.End.Sub(c.Start)
}
Expand All @@ -180,11 +186,11 @@ func (c *Response) ETA() time.Time {
}
bt := c.BytesComplete()
bps := c.transfer.BPS()
if bps == 0 {
if bps <= 0 || c.Size() <= 0 || c.Size() < bt {
return time.Time{}
}
secs := float64(c.Size()-bt) / bps
return time.Now().Add(time.Duration(secs) * time.Second)
return time.Now().Add(time.Duration(secs * float64(time.Second)))
}

// Open blocks the calling goroutine until the underlying file transfer is
Expand Down
40 changes: 40 additions & 0 deletions v3/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,43 @@ func TestResponseBytes(t *testing.T) {
)
})
}

func TestResponseStatsEdgeCases(t *testing.T) {
t.Run("ZeroStartDuration", func(t *testing.T) {
resp := &Response{}
if d := resp.Duration(); d != 0 {
t.Errorf("expected duration 0 for zero start, got %v", d)
}
})

t.Run("BytesPerSecondZeroDuration", func(t *testing.T) {
done := make(chan struct{})
close(done)
resp := &Response{
Done: done,
Start: time.Now(),
End: time.Now(),
}
if bps := resp.BytesPerSecond(); bps != 0 {
t.Errorf("expected 0 bps for 0 duration, got %v", bps)
}
})

t.Run("ETAUnknownSize", func(t *testing.T) {
resp := &Response{
sizeUnsafe: -1,
}
if eta := resp.ETA(); !eta.IsZero() {
t.Errorf("expected zero ETA for unknown size, got %v", eta)
}
})

t.Run("ETAZeroOrNegativeBPS", func(t *testing.T) {
resp := &Response{
sizeUnsafe: 100,
}
if eta := resp.ETA(); !eta.IsZero() {
t.Errorf("expected zero ETA when BPS is 0, got %v", eta)
}
})
}