Skip to content

Commit 847e888

Browse files
authored
fix: non-range HTTP downloaded bytes exceeding total file size (#1317)
1 parent 3d20e81 commit 847e888

8 files changed

Lines changed: 124 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
## 🚀 Introduction
1818

19-
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://gopeed.com/docs/dev-api) or installation and development of [extensions](https://gopeed.com/docs/dev-extension).
19+
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet, ED2K) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://gopeed.com/docs/dev-api) or installation and development of [extensions](https://gopeed.com/docs/dev-extension).
2020

2121
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://gopeed.com/docs)
2222

README_ja-JP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
## 🚀 はじめに
1818

19-
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://gopeed.com/docs/dev-api)との連動や[拡張機能](https://gopeed.com/docs/dev-extension)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
19+
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet、ED2K) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://gopeed.com/docs/dev-api)との連動や[拡張機能](https://gopeed.com/docs/dev-extension)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
2020

2121
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://gopeed.com/docs)
2222

README_vi-VN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
## 🚀 Giới thiệu
1818

19-
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://gopeed.com/docs/dev-api) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://gopeed.com/docs/dev-extension).
19+
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet, ED2K) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://gopeed.com/docs/dev-api) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://gopeed.com/docs/dev-extension).
2020

2121
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://gopeed.com/docs)
2222

README_zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
## 🚀 介绍
1818

19-
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://gopeed.com/docs/dev-api)或者安装和开发[扩展](https://gopeed.com/docs/dev-extension)来实现更多的功能。
19+
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet、ED2K)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://gopeed.com/docs/dev-api)或者安装和开发[扩展](https://gopeed.com/docs/dev-extension)来实现更多的功能。
2020

2121
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://gopeed.com/docs)
2222

README_zh-TW.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
## 🚀 簡介
1818

19-
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
19+
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet、ED2K)協定,同時支援所有的平台。
2020

2121
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://gopeed.com/docs)
2222

internal/protocol/http/fetcher.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ func (f *Fetcher) doStart() error {
618618
for _, conn := range f.connections {
619619
// Reset connections that can be retried
620620
if !conn.Completed && conn.State != connCompleted {
621+
f.resetConnectionForRestart(conn)
621622
conn.State = connNotStarted
622623
conn.failed = false
623624
conn.retryTimes = 0
@@ -1413,6 +1414,27 @@ func (f *Fetcher) helpOtherConnection(helper *connection) bool {
14131414
return true
14141415
}
14151416

1417+
func (f *Fetcher) resetConnectionForRestart(conn *connection) {
1418+
if f.meta.Res.Range {
1419+
return
1420+
}
1421+
1422+
// Without range support a new request always starts from byte 0,
1423+
// so pause/retry must restart instead of continuing from the old offset.
1424+
if conn.Chunk == nil {
1425+
conn.Chunk = newChunk(0, 0)
1426+
} else {
1427+
conn.Chunk.Begin = 0
1428+
conn.Chunk.End = 0
1429+
conn.Chunk.Downloaded = 0
1430+
}
1431+
conn.Downloaded = 0
1432+
conn.Completed = false
1433+
conn.speed = 0
1434+
conn.lastSpeedCheck = 0
1435+
conn.lastSpeedDownload = 0
1436+
}
1437+
14161438
func (f *Fetcher) resumeConnections() {
14171439
// Collect connections to resume while holding the lock
14181440
var toResume []*connection
@@ -1436,6 +1458,7 @@ func (f *Fetcher) resumeConnections() {
14361458
continue
14371459
}
14381460
}
1461+
f.resetConnectionForRestart(conn)
14391462
// Reset the connection state for resume
14401463
conn.ctx, conn.cancel = context.WithCancel(f.ctx)
14411464
conn.State = connNotStarted
@@ -1475,7 +1498,16 @@ func (f *Fetcher) onDownloadComplete() {
14751498
// Check if all chunks are complete (no remaining bytes)
14761499
allChunksComplete := true
14771500
for _, conn := range f.connections {
1478-
if conn.Chunk != nil && conn.Chunk.remain() > 0 && !conn.Completed && conn.State != connCompleted {
1501+
needsMoreData := false
1502+
if f.meta.Res.Range {
1503+
needsMoreData = conn.Chunk != nil && conn.Chunk.remain() > 0
1504+
} else if f.meta.Res.Size > 0 {
1505+
needsMoreData = conn.Downloaded < f.meta.Res.Size
1506+
} else {
1507+
needsMoreData = !conn.Completed && conn.State != connCompleted
1508+
}
1509+
1510+
if needsMoreData && !conn.Completed && conn.State != connCompleted {
14791511
// This connection has remaining work and isn't done
14801512
// Check if it failed with 403 (server limit) - these can be ignored if other connections completed the work
14811513
if conn.State == connFailed && conn.failed {

internal/protocol/http/fetcher_test.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,53 @@ func TestFetcher_DownloadContinue(t *testing.T) {
299299
downloadContinue(listener, 16, t)
300300
}
301301

302+
func TestFetcher_DownloadContinue_NoRangeRestart(t *testing.T) {
303+
listener := test.StartTestNoRangeSlowServer(time.Millisecond)
304+
defer listener.Close()
305+
306+
fetcher := downloadReady(listener, 4, t)
307+
if err := fetcher.Start(); err != nil {
308+
t.Fatal(err)
309+
}
310+
311+
time.Sleep(20 * time.Millisecond)
312+
313+
stats := fetcher.Stats().(*http.Stats)
314+
if len(stats.Connections) != 1 {
315+
t.Fatalf("expected a single non-range connection, got %d", len(stats.Connections))
316+
}
317+
if stats.Connections[0].Downloaded <= 0 || stats.Connections[0].Downloaded >= test.BuildSize {
318+
t.Fatalf("expected partial download before pause, got %d", stats.Connections[0].Downloaded)
319+
}
320+
321+
if err := fetcher.Pause(); err != nil {
322+
t.Fatal(err)
323+
}
324+
if err := fetcher.Start(); err != nil {
325+
t.Fatal(err)
326+
}
327+
if err := fetcher.Wait(); err != nil {
328+
t.Fatal(err)
329+
}
330+
331+
finalStats := fetcher.Stats().(*http.Stats)
332+
if len(finalStats.Connections) != 1 {
333+
t.Fatalf("expected a single non-range connection after resume, got %d", len(finalStats.Connections))
334+
}
335+
if finalStats.Connections[0].Downloaded != test.BuildSize {
336+
t.Fatalf("downloaded bytes should restart cleanly: got %d, want %d", finalStats.Connections[0].Downloaded, test.BuildSize)
337+
}
338+
if total := fetcher.Progress().TotalDownloaded(); total != test.BuildSize {
339+
t.Fatalf("progress total = %d, want %d", total, test.BuildSize)
340+
}
341+
342+
want := test.FileMd5(test.BuildFile)
343+
got := test.FileMd5(test.DownloadFile)
344+
if want != got {
345+
t.Errorf("Download() got = %v, want %v", got, want)
346+
}
347+
}
348+
302349
func TestFetcher_DownloadChunked(t *testing.T) {
303350
listener := test.StartTestCustomServer()
304351
defer listener.Close()
@@ -1384,8 +1431,8 @@ func TestFetcher_Patch_Extra(t *testing.T) {
13841431
Method: "POST", // Update method
13851432
// Body is empty, should NOT update
13861433
Header: map[string]string{
1387-
"X-Custom": "modified", // Overwrite existing
1388-
"X-New": "added", // Add new
1434+
"X-Custom": "modified", // Overwrite existing
1435+
"X-New": "added", // Add new
13891436
// Authorization is not in patch, should remain
13901437
},
13911438
},

internal/test/httptest.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,43 @@ func StartTestOneTimeServer() net.Listener {
335335
})
336336
}
337337

338+
// StartTestNoRangeSlowServer creates a server that always returns the full file
339+
// with Content-Length but does not support Range requests.
340+
func StartTestNoRangeSlowServer(delayPerChunk time.Duration) net.Listener {
341+
return startTestServer(func(sl *shutdownListener) http.Handler {
342+
mux := http.NewServeMux()
343+
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
344+
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
345+
346+
file, err := os.Open(BuildFile)
347+
if err != nil {
348+
return
349+
}
350+
defer file.Close()
351+
352+
buf := make([]byte, 256*1024)
353+
for !sl.isShutdown {
354+
n, readErr := file.Read(buf)
355+
if n > 0 {
356+
if _, writeErr := writer.Write(buf[:n]); writeErr != nil {
357+
return
358+
}
359+
if flusher, ok := writer.(http.Flusher); ok {
360+
flusher.Flush()
361+
}
362+
if delayPerChunk > 0 {
363+
time.Sleep(delayPerChunk)
364+
}
365+
}
366+
if readErr != nil {
367+
return
368+
}
369+
}
370+
})
371+
return mux
372+
})
373+
}
374+
338375
// StartTestExpiringRedirectServer creates a server that simulates expiring redirect URLs.
339376
// The original URL redirects to a temporary URL that expires after a specified number of requests.
340377
// When the temporary URL expires (returns 403), the client should retry with the original URL

0 commit comments

Comments
 (0)