Skip to content

Commit a44a1fb

Browse files
committed
fix header issue
Signed-off-by: abzcoding <[email protected]>
1 parent a2eed9e commit a44a1fb

File tree

7 files changed

+716
-69
lines changed

7 files changed

+716
-69
lines changed

e2e_test.go

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"os"
9+
"os/user"
10+
"path/filepath"
11+
"strconv"
12+
"strings"
13+
"sync/atomic"
14+
"testing"
15+
"time"
16+
)
17+
18+
// helper to create deterministic content
19+
func makeContent(size int) []byte {
20+
data := make([]byte, size)
21+
for i := 0; i < size; i++ {
22+
data[i] = byte('A' + (i % 23))
23+
}
24+
return data
25+
}
26+
27+
// start a configurable HTTP server
28+
func startTestServer(t *testing.T, content []byte, supportRange bool, headShowsAcceptRanges bool, headHasContentLength bool, getHasContentLength bool, path string) (*httptest.Server, *int32) {
29+
var rangeRequests int32
30+
31+
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32+
if r.URL.Path != path {
33+
w.WriteHeader(http.StatusNotFound)
34+
return
35+
}
36+
37+
switch r.Method {
38+
case http.MethodHead:
39+
if headShowsAcceptRanges && supportRange {
40+
w.Header().Set("Accept-Ranges", "bytes")
41+
}
42+
if headHasContentLength {
43+
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
44+
}
45+
w.WriteHeader(http.StatusOK)
46+
return
47+
case http.MethodGet:
48+
// Range probe
49+
rangeHeader := r.Header.Get("Range")
50+
if supportRange && rangeHeader != "" && strings.HasPrefix(rangeHeader, "bytes=") {
51+
atomic.AddInt32(&rangeRequests, 1)
52+
// parse bytes=start-end
53+
spec := strings.TrimPrefix(rangeHeader, "bytes=")
54+
parts := strings.SplitN(spec, "-", 2)
55+
start, _ := strconv.ParseInt(parts[0], 10, 64)
56+
var end int64
57+
if parts[1] == "" {
58+
end = int64(len(content) - 1)
59+
} else {
60+
end, _ = strconv.ParseInt(parts[1], 10, 64)
61+
}
62+
if start < 0 {
63+
start = 0
64+
}
65+
if end >= int64(len(content)) {
66+
end = int64(len(content) - 1)
67+
}
68+
if start > end {
69+
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
70+
return
71+
}
72+
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(content)))
73+
if getHasContentLength {
74+
w.Header().Set("Content-Length", strconv.Itoa(int(end-start+1)))
75+
}
76+
w.WriteHeader(http.StatusPartialContent)
77+
_, _ = w.Write(content[start : end+1])
78+
return
79+
}
80+
// regular GET
81+
if getHasContentLength {
82+
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
83+
}
84+
w.WriteHeader(http.StatusOK)
85+
_, _ = w.Write(content)
86+
return
87+
default:
88+
w.WriteHeader(http.StatusMethodNotAllowed)
89+
}
90+
})
91+
92+
ts := httptest.NewServer(h)
93+
t.Cleanup(ts.Close)
94+
return ts, &rangeRequests
95+
}
96+
97+
func withTempCwd(t *testing.T) func() {
98+
old, err := os.Getwd()
99+
if err != nil {
100+
t.Fatalf("Getwd failed: %v", err)
101+
}
102+
tdir := t.TempDir()
103+
if err := os.Chdir(tdir); err != nil {
104+
t.Fatalf("Chdir failed: %v", err)
105+
}
106+
return func() { _ = os.Chdir(old) }
107+
}
108+
109+
func withTestDataFolder(t *testing.T) func() {
110+
original := dataFolder
111+
dataFolder = ".hget_e2e_test/"
112+
return func() {
113+
dataFolder = original
114+
usr, _ := user.Current()
115+
_ = os.RemoveAll(filepath.Join(usr.HomeDir, dataFolder))
116+
}
117+
}
118+
119+
func TestE2EParallelDownloadWithRange(t *testing.T) {
120+
displayProgress = false
121+
restoreCwd := withTempCwd(t)
122+
defer restoreCwd()
123+
restoreDF := withTestDataFolder(t)
124+
defer restoreDF()
125+
126+
content := makeContent(256 * 1024)
127+
path := "/file.bin"
128+
ts, _ := startTestServer(t, content, true, true, true, true, path)
129+
130+
url := ts.URL + path
131+
Execute(url, nil, 4, false, "", "")
132+
133+
// Verify output file exists and content matches
134+
out := TaskFromURL(url)
135+
got, err := os.ReadFile(out)
136+
if err != nil {
137+
t.Fatalf("failed to read output: %v", err)
138+
}
139+
if !bytes.Equal(got, content) {
140+
t.Fatalf("downloaded content mismatch")
141+
}
142+
143+
// Ensure data folder cleaned up
144+
usr, _ := user.Current()
145+
folder := filepath.Join(usr.HomeDir, dataFolder, TaskFromURL(url))
146+
if _, err := os.Stat(folder); !os.IsNotExist(err) {
147+
t.Fatalf("expected data folder removed, got err=%v", err)
148+
}
149+
}
150+
151+
func TestE2EResumeDownload(t *testing.T) {
152+
displayProgress = false
153+
restoreCwd := withTempCwd(t)
154+
defer restoreCwd()
155+
restoreDF := withTestDataFolder(t)
156+
defer restoreDF()
157+
158+
content := makeContent(300 * 1024)
159+
path := "/resume.bin"
160+
ts, _ := startTestServer(t, content, true, true, true, true, path)
161+
url := ts.URL + path
162+
163+
// Prepare partial state files inside the expected folder
164+
parts := partCalculate(4, int64(len(content)), url)
165+
folder := FolderOf(url)
166+
if err := MkdirIfNotExist(folder); err != nil {
167+
t.Fatalf("failed to create folder: %v", err)
168+
}
169+
for i := range parts {
170+
// write half of the part's size
171+
start := parts[i].RangeFrom
172+
end := parts[i].RangeTo
173+
if end == int64(len(content)) { // last part uses open end, approximate size
174+
end = int64(len(content) - 1)
175+
}
176+
partSize := end - start + 1
177+
writeSize := partSize / 2
178+
if writeSize <= 0 {
179+
writeSize = 1
180+
}
181+
slice := content[start : start+writeSize]
182+
if err := os.WriteFile(parts[i].Path, slice, 0600); err != nil {
183+
t.Fatalf("failed to write partial part: %v", err)
184+
}
185+
}
186+
187+
state := &State{URL: url, Parts: parts}
188+
if err := state.Save(); err != nil {
189+
t.Fatalf("failed to save state: %v", err)
190+
}
191+
192+
// Resume will bump RangeFrom by the existing file sizes
193+
resumed, err := Resume(url)
194+
if err != nil {
195+
t.Fatalf("Resume failed: %v", err)
196+
}
197+
198+
Execute(url, resumed, 4, false, "", "")
199+
200+
out := TaskFromURL(url)
201+
got, err := os.ReadFile(out)
202+
if err != nil {
203+
t.Fatalf("failed to read output: %v", err)
204+
}
205+
if !bytes.Equal(got, content) {
206+
t.Fatalf("resumed content mismatch")
207+
}
208+
}
209+
210+
func TestE2ERangeSupportedWithoutAcceptRanges(t *testing.T) {
211+
displayProgress = false
212+
restoreCwd := withTempCwd(t)
213+
defer restoreCwd()
214+
restoreDF := withTestDataFolder(t)
215+
defer restoreDF()
216+
217+
content := makeContent(128 * 1024)
218+
path := "/noar.bin"
219+
ts, rangeCount := startTestServer(t, content, true, false, true, true, path)
220+
url := ts.URL + path
221+
222+
Execute(url, nil, 3, false, "", "")
223+
224+
// Confirm at least one ranged request happened
225+
if atomic.LoadInt32(rangeCount) == 0 {
226+
t.Fatalf("expected ranged requests when Accept-Ranges absent")
227+
}
228+
// Validate file
229+
out := TaskFromURL(url)
230+
got, err := os.ReadFile(out)
231+
if err != nil || !bytes.Equal(got, content) {
232+
t.Fatalf("download mismatch or error: %v", err)
233+
}
234+
}
235+
236+
func TestE2EUnknownLengthSinglePart(t *testing.T) {
237+
displayProgress = false
238+
restoreCwd := withTempCwd(t)
239+
defer restoreCwd()
240+
restoreDF := withTestDataFolder(t)
241+
defer restoreDF()
242+
243+
content := makeContent(64 * 1024)
244+
path := "/chunked.bin"
245+
// no Accept-Ranges, no Content-Length on HEAD or GET
246+
ts, _ := startTestServer(t, content, false, false, false, false, path)
247+
url := ts.URL + path
248+
249+
Execute(url, nil, 4, false, "", "")
250+
251+
out := TaskFromURL(url)
252+
got, err := os.ReadFile(out)
253+
if err != nil || !bytes.Equal(got, content) {
254+
t.Fatalf("download mismatch or error: %v", err)
255+
}
256+
}
257+
258+
func TestE2EGlobalRateLimit(t *testing.T) {
259+
displayProgress = false
260+
restoreCwd := withTempCwd(t)
261+
defer restoreCwd()
262+
restoreDF := withTestDataFolder(t)
263+
defer restoreDF()
264+
265+
content := makeContent(200 * 1024) // 200KB
266+
path := "/rate.bin"
267+
ts, _ := startTestServer(t, content, true, true, true, true, path)
268+
url := ts.URL + path
269+
270+
start := time.Now()
271+
Execute(url, nil, 4, false, "", "100KB")
272+
dur := time.Since(start)
273+
274+
// With global 100KB/s limit and a 100KB burst, 200KB typically completes ~1.0–1.1s.
275+
// Assert only that we are not effectively unthrottled (<0.9s would be suspicious on CI).
276+
if dur < 900*time.Millisecond {
277+
t.Fatalf("rate limiting too fast: %v", dur)
278+
}
279+
280+
out := TaskFromURL(url)
281+
got, err := os.ReadFile(out)
282+
if err != nil || !bytes.Equal(got, content) {
283+
t.Fatalf("download mismatch or error: %v", err)
284+
}
285+
}

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ require (
99
github.com/imkira/go-task v1.0.0
1010
github.com/mattn/go-colorable v0.1.14
1111
github.com/mattn/go-isatty v0.0.20
12-
golang.org/x/net v0.37.0
12+
golang.org/x/net v0.43.0
13+
golang.org/x/time v0.12.0
1314
gopkg.in/cheggaaa/pb.v1 v1.0.28
1415
)
1516

1617
require (
1718
github.com/mattn/go-runewidth v0.0.16 // indirect
1819
github.com/rivo/uniseg v0.4.7 // indirect
19-
golang.org/x/sys v0.31.0 // indirect
20-
golang.org/x/time v0.10.0 // indirect
20+
golang.org/x/sys v0.35.0 // indirect
2121
)

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
3131
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
3232
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
3333
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
34-
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
35-
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
34+
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
35+
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
3636
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
37-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
38-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
37+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
38+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
3939
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
40-
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
41-
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
40+
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
41+
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
4242
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4343
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
4444
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=

0 commit comments

Comments
 (0)