Skip to content

Commit 9ed4a2c

Browse files
authored
Merge branch 'main' into copilot/add-auto-move-file-on-download
2 parents 3ee6593 + 3eb6df5 commit 9ed4a2c

26 files changed

Lines changed: 1335 additions & 143 deletions

File tree

.github/workflows/build.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,17 @@ jobs:
4444
- arch: arm64
4545
os: windows-11-arm
4646
llvm_ver: "20251202" # Only ARM64
47-
flutter_channel: "master"
48-
flutter_version: "8817d45220d" # Only ARM64
47+
flutter_channel: "main"
48+
flutter_version: "7e1c8868"
4949
- arch: amd64
5050
os: windows-2022
51-
flutter_channel: "stable"
5251
runs-on: ${{ matrix.os }}
5352

5453
steps:
5554
- uses: actions/checkout@v3
55+
- name: Enable long paths for flutter main branch checks
56+
run: |
57+
git config --global core.longpaths true
5658
5759
# Install LLVM environment only on ARM64
5860
- name: Install llvm-mingw-ucrt-aarch64 (ARM64 only)
@@ -84,7 +86,7 @@ jobs:
8486
go-version: ${{ env.GO_VERSION }}
8587
- uses: subosito/flutter-action@v2
8688
with:
87-
channel: ${{ matrix.flutter_channel }}
89+
channel: ${{ matrix.flutter_channel || 'stable' }}
8890
flutter-version: ${{ matrix.flutter_version || env.FLUTTER_VERSION }}
8991
- name: Build
9092
env:
@@ -112,6 +114,7 @@ jobs:
112114
$release = "build\windows\arm64\runner\Release\"
113115
$mingw = "$env:CLANGARM64_ROOT\aarch64-w64-mingw32\bin"
114116
cp $mingw\libc++.dll $release
117+
cp $mingw\libunwind.dll $release
115118
}
116119
cp $mingw\libwinpthread-1.dll $release
117120
cp $system\msvcp140.dll $release

.github/workflows/test.yml

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,56 @@ jobs:
6666
files: ./coverage.txt
6767
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
6868
build-desktop:
69-
name: Build desktop check
70-
runs-on: ${{ matrix.os }}
7169
strategy:
7270
matrix:
73-
os: [windows-2022, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
71+
include:
72+
- os: windows-2022
73+
- os: windows-11-arm
74+
llvm_ver: "20251202" # Only ARM64
75+
flutter_channel: "main"
76+
flutter_version: "7e1c8868"
77+
- os: macos-latest
78+
- os: ubuntu-22.04
79+
- os: ubuntu-22.04-arm
80+
flutter_channel: "main"
81+
name: Build desktop check (${{ matrix.os }})
82+
runs-on: ${{ matrix.os }}
7483
needs: [test]
7584
steps:
7685
- uses: actions/checkout@v3
86+
- name: Enable long paths for flutter main branch checks
87+
run: |
88+
git config --global core.longpaths true
89+
- name: Install llvm-mingw-ucrt-aarch64 (ARM64 only)
90+
if: matrix.os == 'windows-11-arm'
91+
run: |
92+
$ver = "${{ matrix.llvm_ver }}"
93+
$url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$ver/llvm-mingw-$ver-ucrt-aarch64.zip"
94+
$zip = "$env:RUNNER_TEMP\\llvm.zip"
95+
$extract = "$env:RUNNER_TEMP\\extract"
96+
$target = "C:\\clangarm64"
97+
98+
curl -L $url -o $zip
99+
rm -r -fo $extract,$target -ea Ignore
100+
mkdir $extract | Out-Null
101+
102+
tar -xf $zip -C $extract
103+
mv (Get-ChildItem $extract)[0].FullName $target
104+
105+
$b = "$target\\bin"
106+
"CC=$b\\clang.exe" >> $env:GITHUB_ENV
107+
"CXX=$b\\clang++.exe" >> $env:GITHUB_ENV
108+
"CLANGARM64_BIN=$b" >> $env:GITHUB_ENV
109+
"CGO_ENABLED=1" >> $env:GITHUB_ENV
110+
"CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV
111+
$b >> $env:GITHUB_PATH
77112
- uses: actions/setup-go@v4
78113
with:
79114
go-version: ${{ env.GO_VERSION }}
80115
- uses: subosito/flutter-action@v2
81116
with:
82-
flutter-version: ${{ env.FLUTTER_VERSION }}
83-
channel: master
117+
channel: ${{ matrix.flutter_channel || 'stable' }}
118+
flutter-version: ${{ matrix.flutter_version || env.FLUTTER_VERSION }}
84119
- if: runner.os == 'Windows'
85120
run: |
86121
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.24.11 AS go
1+
FROM golang:1.24.11-alpine3.23 AS go
22
WORKDIR /app
33
COPY ./go.mod ./go.sum ./
44
RUN go mod download
@@ -8,7 +8,7 @@ RUN CGO_ENABLED=0 go build -tags nosqlite,web \
88
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
99
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
1010

11-
FROM alpine:3.18
11+
FROM alpine:3.23
1212
LABEL maintainer="monkeyWie"
1313
WORKDIR /app
1414
COPY --from=go /app/dist/gopeed ./

internal/fetcher/fetcher.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ type Fetcher interface {
1414
Setup(ctl *controller.Controller)
1515
Resolve(req *base.Request, opts *base.Options) error
1616
Start() error
17+
// Patch modifies task-specific data based on the protocol.
18+
// For HTTP: can modify Request info (URL, headers, etc.)
19+
// For BT: can modify SelectFiles (via opts.SelectFiles)
20+
Patch(req *base.Request, opts *base.Options) error
1721
Pause() error
1822
Close() error
1923

internal/protocol/bt/fetcher.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,59 @@ func (f *Fetcher) isDone() bool {
232232
return true
233233
}
234234

235+
// Patch modifies the BT task settings.
236+
// Invalid file indices are silently ignored.
237+
func (f *Fetcher) Patch(req *base.Request, opts *base.Options) error {
238+
if opts == nil {
239+
return nil
240+
}
241+
242+
if opts.SelectFiles != nil {
243+
selectFiles := opts.SelectFiles
244+
245+
// Get file count from resource metadata
246+
fileCount := 0
247+
if f.meta.Res != nil {
248+
fileCount = len(f.meta.Res.Files)
249+
}
250+
251+
// Filter out invalid indices (silently ignore)
252+
validSelectFiles := make([]int, 0, len(selectFiles))
253+
for _, idx := range selectFiles {
254+
if idx >= 0 && idx < fileCount {
255+
validSelectFiles = append(validSelectFiles, idx)
256+
}
257+
}
258+
259+
if f.torrent != nil {
260+
files := f.torrent.Files()
261+
262+
// Cancel all current file downloads first
263+
f.torrent.CancelPieces(0, f.torrent.NumPieces())
264+
265+
// Apply new file selection
266+
if len(validSelectFiles) == len(files) {
267+
f.torrent.DownloadAll()
268+
} else {
269+
for _, selectIndex := range validSelectFiles {
270+
file := files[selectIndex]
271+
file.Download()
272+
}
273+
}
274+
}
275+
276+
f.meta.Opts.SelectFiles = validSelectFiles
277+
// Recalculate the resource size based on new selection
278+
if f.meta.Res != nil {
279+
f.meta.Res.CalcSize(validSelectFiles)
280+
}
281+
// Reset progress tracking for new file selection
282+
f.data.Progress = make(fetcher.Progress, len(validSelectFiles))
283+
}
284+
285+
return nil
286+
}
287+
235288
func (f *Fetcher) updateRes() {
236289
res := &base.Resource{
237290
Range: true,

internal/protocol/bt/fetcher_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,130 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher
239239
fetcher.Setup(newController)
240240
return fetcher
241241
}
242+
243+
// TestFetcher_Patch tests the Patch functionality for BT fetcher.
244+
// It tests modifying selected files after Resolve (without downloading).
245+
func TestFetcher_Patch(t *testing.T) {
246+
f := buildFetcher()
247+
248+
// Resolve a multi-file torrent
249+
err := f.Resolve(&base.Request{
250+
URL: "./testdata/test.torrent",
251+
}, nil)
252+
if err != nil {
253+
t.Fatalf("Resolve failed: %v", err)
254+
}
255+
256+
// Verify initial state: 3 files, all selected by default
257+
meta := f.Meta()
258+
if len(meta.Res.Files) != 3 {
259+
t.Fatalf("Expected 3 files, got %d", len(meta.Res.Files))
260+
}
261+
if len(meta.Opts.SelectFiles) != 3 {
262+
t.Fatalf("Expected 3 selected files, got %d", len(meta.Opts.SelectFiles))
263+
}
264+
265+
// Total size of all files: 107484864
266+
totalSize := int64(107484864)
267+
if meta.Res.Size != totalSize {
268+
t.Fatalf("Expected total size %d, got %d", totalSize, meta.Res.Size)
269+
}
270+
271+
t.Run("Patch with valid indices", func(t *testing.T) {
272+
// Select only file 0 and 2 (c.txt and a.txt)
273+
err := f.Patch(nil, &base.Options{
274+
SelectFiles: []int{0, 2},
275+
})
276+
if err != nil {
277+
t.Fatalf("Patch failed: %v", err)
278+
}
279+
280+
meta := f.Meta()
281+
if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 2}) {
282+
t.Errorf("Expected SelectFiles [0, 2], got %v", meta.Opts.SelectFiles)
283+
}
284+
285+
// Size should be recalculated: c.txt (98501754) + a.txt (78114) = 98579868
286+
expectedSize := int64(98501754 + 78114)
287+
if meta.Res.Size != expectedSize {
288+
t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size)
289+
}
290+
})
291+
292+
t.Run("Patch with invalid indices are silently ignored", func(t *testing.T) {
293+
// Mix of valid (0, 1) and invalid (-1, 5, 100) indices
294+
err := f.Patch(nil, &base.Options{
295+
SelectFiles: []int{-1, 0, 5, 1, 100},
296+
})
297+
if err != nil {
298+
t.Fatalf("Patch should not return error for invalid indices: %v", err)
299+
}
300+
301+
meta := f.Meta()
302+
// Only valid indices 0 and 1 should remain
303+
if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 1}) {
304+
t.Errorf("Expected SelectFiles [0, 1], got %v", meta.Opts.SelectFiles)
305+
}
306+
307+
// Size should be: c.txt (98501754) + b.txt (8904996) = 107406750
308+
expectedSize := int64(98501754 + 8904996)
309+
if meta.Res.Size != expectedSize {
310+
t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size)
311+
}
312+
})
313+
314+
t.Run("Patch with all invalid indices results in empty selection", func(t *testing.T) {
315+
err := f.Patch(nil, &base.Options{
316+
SelectFiles: []int{-5, 10, 999},
317+
})
318+
if err != nil {
319+
t.Fatalf("Patch should not return error: %v", err)
320+
}
321+
322+
meta := f.Meta()
323+
if len(meta.Opts.SelectFiles) != 0 {
324+
t.Errorf("Expected empty SelectFiles, got %v", meta.Opts.SelectFiles)
325+
}
326+
327+
// Note: CalcSize with empty selectFiles calculates total size of all files
328+
// This is by design - empty selection in CalcSize means "all files"
329+
// But SelectFiles being empty means no files are selected for download
330+
if meta.Res.Size != totalSize {
331+
t.Errorf("Expected size %d (CalcSize with empty slice = all files), got %d", totalSize, meta.Res.Size)
332+
}
333+
})
334+
335+
t.Run("Patch with nil opts does nothing", func(t *testing.T) {
336+
// First set a known state
337+
f.Patch(nil, &base.Options{SelectFiles: []int{1}})
338+
prevSelectFiles := f.Meta().Opts.SelectFiles
339+
340+
// Patch with nil opts
341+
err := f.Patch(nil, nil)
342+
if err != nil {
343+
t.Fatalf("Patch with nil opts should not fail: %v", err)
344+
}
345+
346+
// Should remain unchanged
347+
if !reflect.DeepEqual(f.Meta().Opts.SelectFiles, prevSelectFiles) {
348+
t.Errorf("SelectFiles should remain unchanged after nil opts Patch")
349+
}
350+
})
351+
352+
t.Run("Patch progress array is resized", func(t *testing.T) {
353+
btFetcher := f.(*Fetcher)
354+
// Initialize progress array
355+
btFetcher.data.Progress = make(fetcher.Progress, 3)
356+
357+
err := f.Patch(nil, &base.Options{
358+
SelectFiles: []int{0, 2},
359+
})
360+
if err != nil {
361+
t.Fatalf("Patch failed: %v", err)
362+
}
363+
364+
if len(btFetcher.data.Progress) != 2 {
365+
t.Errorf("Expected Progress length 2, got %d", len(btFetcher.data.Progress))
366+
}
367+
})
368+
}

internal/protocol/http/fetcher.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,9 @@ func (f *Fetcher) downloadChunkOnce(conn *connection, client *http.Client, buf [
10211021
// Fallback succeeded, use this response instead
10221022
resp = fallbackResp
10231023
// Update the redirect URL from the response
1024-
f.updateRedirectURL(resp)
1024+
if resp.Request != nil && resp.Request.URL != nil {
1025+
f.updateRedirectURL(resp.Request.URL.String())
1026+
}
10251027
} else {
10261028
// Fallback also failed, return the original error
10271029
if fallbackResp != nil {
@@ -1571,6 +1573,60 @@ func (f *Fetcher) checkCompletion() bool {
15711573
return false
15721574
}
15731575

1576+
// Patch modifies the HTTP request information.
1577+
func (f *Fetcher) Patch(req *base.Request, opts *base.Options) error {
1578+
// Patch request info
1579+
if req != nil {
1580+
if req.URL != "" {
1581+
f.meta.Req.URL = req.URL
1582+
// Clear redirect URL when URL is changed, so new requests use the new URL
1583+
f.updateRedirectURL("")
1584+
}
1585+
if req.Extra != nil {
1586+
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
1587+
return err
1588+
}
1589+
patchExtra := req.Extra.(*fhttp.ReqExtra)
1590+
// Merge Extra fields instead of replacing entirely
1591+
if f.meta.Req.Extra == nil {
1592+
f.meta.Req.Extra = &fhttp.ReqExtra{}
1593+
}
1594+
existingExtra := f.meta.Req.Extra.(*fhttp.ReqExtra)
1595+
// Update Method only if non-empty
1596+
if patchExtra.Method != "" {
1597+
existingExtra.Method = patchExtra.Method
1598+
}
1599+
// Update Body only if non-empty
1600+
if patchExtra.Body != "" {
1601+
existingExtra.Body = patchExtra.Body
1602+
}
1603+
// Merge Headers: existing keys are overwritten, new keys are added
1604+
if patchExtra.Header != nil {
1605+
if existingExtra.Header == nil {
1606+
existingExtra.Header = make(map[string]string)
1607+
}
1608+
for k, v := range patchExtra.Header {
1609+
existingExtra.Header[k] = v
1610+
}
1611+
}
1612+
}
1613+
// Merge Labels: existing keys are overwritten, new keys are added
1614+
if req.Labels != nil {
1615+
if f.meta.Req.Labels == nil {
1616+
f.meta.Req.Labels = make(map[string]string)
1617+
}
1618+
for k, v := range req.Labels {
1619+
f.meta.Req.Labels[k] = v
1620+
}
1621+
}
1622+
if req.Proxy != nil {
1623+
f.meta.Req.Proxy = req.Proxy
1624+
}
1625+
}
1626+
1627+
return nil
1628+
}
1629+
15741630
func (f *Fetcher) Pause() error {
15751631
if f.cancel != nil {
15761632
f.cancel()

0 commit comments

Comments
 (0)