mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 11:24:08 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4b4062e81 | ||
|
|
392ee36137 | ||
|
|
a34383a7e2 | ||
|
|
98879569f0 | ||
|
|
8ef3b63e8d | ||
|
|
d873cd411c |
No files matched your search
@@ -13,7 +13,7 @@ on:
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.25.4"
|
||||
GO_VERSION: "1.26.11"
|
||||
FLUTTER_VERSION: "3.41.6"
|
||||
GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }}
|
||||
|
||||
@@ -17,7 +17,7 @@ on:
|
||||
- "Dockerfile"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.25.4"
|
||||
GO_VERSION: "1.26.11"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -28,7 +28,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.25.4"
|
||||
GO_VERSION: "1.26.11"
|
||||
FLUTTER_VERSION: "3.41.6"
|
||||
|
||||
jobs:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25.4-alpine3.22 AS go
|
||||
FROM golang:1.26.11-alpine3.23 AS go
|
||||
WORKDIR /app
|
||||
COPY ./go.mod ./go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
module github.com/GopeedLab/gopeed
|
||||
|
||||
go 1.25.4
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
codeberg.org/gruf/go-ffmpreg v0.6.23-0.20260827095416-df1836cb5be0
|
||||
github.com/GopeedLab/webview_go v0.0.0-20260918010912-0d4c27bb7914
|
||||
github.com/anacrolix/torrent v1.60.1-0.20251217073903-486bcbe758e0
|
||||
github.com/RoaringBitmap/roaring/v2 v2.16.0
|
||||
github.com/anacrolix/torrent v1.62.0
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/bodgit/sevenzip v1.6.1
|
||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
|
||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||
github.com/fclairamb/ftpserverlib v0.31.0
|
||||
github.com/go-git/go-git/v5 v5.8.1
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/google/jsonschema-go v0.4.3
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/imroc/req/v3 v3.52.2
|
||||
github.com/imroc/req/v3 v3.58.0
|
||||
github.com/jlaffaye/ftp v0.2.4
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/mattn/go-ieproxy v0.0.12
|
||||
@@ -29,62 +30,61 @@ require (
|
||||
github.com/tetratelabs/wazero v1.12.0
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/onsi/gomega v1.36.3 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/anacrolix/btree v0.1.1 // indirect
|
||||
github.com/anacrolix/missinggo/v2 v2.10.0 // indirect
|
||||
github.com/anacrolix/go-utp v0.0.0-20260908033909-9f1664acf866 // indirect
|
||||
github.com/anacrolix/missinggo/v2 v2.11.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coder/websocket v1.8.15 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/icholy/digest v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.51.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/pion/transport/v4 v4.0.2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.1 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/mod v0.33.0
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.9.4
|
||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||
github.com/anacrolix/chansync v0.7.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.23.0 // indirect
|
||||
github.com/anacrolix/chansync v0.8.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.23.1-0.20260525063928-ec3a9bd99456 // indirect
|
||||
github.com/anacrolix/envpprof v1.5.0 // indirect
|
||||
github.com/anacrolix/generics v0.2.0 // indirect
|
||||
github.com/anacrolix/go-libutp v1.3.2 // indirect
|
||||
github.com/anacrolix/generics v0.2.0
|
||||
github.com/anacrolix/go-libutp v1.5.2-0.20260908013213-836dd42cdde6 // indirect
|
||||
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||
@@ -93,13 +93,12 @@ require (
|
||||
github.com/anacrolix/stm v0.5.0 // indirect
|
||||
github.com/anacrolix/sync v0.6.0 // indirect
|
||||
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||
github.com/anacrolix/utp v0.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -107,17 +106,16 @@ require (
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.2 // indirect
|
||||
github.com/go-llsqlite/adapter v0.2.0 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
@@ -131,7 +129,7 @@ require (
|
||||
github.com/multiformats/go-varint v0.1.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pion/datachannel v1.6.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.4 // indirect
|
||||
github.com/pion/ice/v4 v4.2.0 // indirect
|
||||
github.com/pion/interceptor v0.1.42 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
@@ -142,28 +140,27 @@ require (
|
||||
github.com/pion/sctp v1.9.1 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.17 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||
github.com/pion/stun/v3 v3.1.1 // indirect
|
||||
github.com/pion/stun/v3 v3.1.5 // indirect
|
||||
github.com/pion/turn/v4 v4.1.4 // indirect
|
||||
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/tidwall/btree v1.8.1 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/net v0.60.0
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0
|
||||
golang.org/x/time v0.15.0
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
modernc.org/libc v1.67.4 // indirect
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/RoaringBitmap/roaring"
|
||||
"github.com/RoaringBitmap/roaring/v2"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
protocolbt "github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
"github.com/RoaringBitmap/roaring"
|
||||
"github.com/RoaringBitmap/roaring/v2"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
)
|
||||
|
||||
@@ -314,6 +314,7 @@ type Fetcher struct {
|
||||
prefetchDone atomic.Bool // Prefetch completed or stopped
|
||||
prefetchErr error // Error during prefetch (if any)
|
||||
prefetchStopCh chan struct{} // Signal to stop prefetch
|
||||
prefetchExitCh chan struct{} // Closed when the prefetch goroutine exits
|
||||
resolveFallback atomic.Bool // Resolve prefetch is still available until the first Range request takes over
|
||||
|
||||
// Target file
|
||||
@@ -562,6 +563,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
// For non-range resources, the response will be used directly in Start
|
||||
if res.Range && reuseResolveBody {
|
||||
f.prefetchStopCh = make(chan struct{})
|
||||
f.prefetchExitCh = make(chan struct{})
|
||||
f.resolveFallback.Store(true)
|
||||
go f.asyncPrefetch()
|
||||
}
|
||||
@@ -579,6 +581,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
func (f *Fetcher) asyncPrefetch() {
|
||||
defer func() {
|
||||
f.prefetchDone.Store(true)
|
||||
if f.prefetchExitCh != nil {
|
||||
close(f.prefetchExitCh)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get the resolve response
|
||||
@@ -654,23 +659,38 @@ func (f *Fetcher) asyncPrefetch() {
|
||||
}
|
||||
}
|
||||
|
||||
// stopPrefetch signals the async prefetch to stop and waits until it exits.
|
||||
// Closing the resolve body unblocks a pending Read, so a stalled server cannot
|
||||
// keep the prefetch goroutine writing to the temporary file after we reuse it.
|
||||
func (f *Fetcher) stopPrefetch() {
|
||||
if f.prefetchStopCh == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Pause and the first connection's takeover can stop concurrently, so the
|
||||
// check-and-close must be under the lock to avoid closing the channel twice.
|
||||
f.resolveRespLock.Lock()
|
||||
select {
|
||||
case <-f.prefetchStopCh:
|
||||
// Already closed
|
||||
default:
|
||||
close(f.prefetchStopCh)
|
||||
}
|
||||
if f.resolveResp != nil {
|
||||
f.resolveResp.Body.Close()
|
||||
f.resolveResp = nil
|
||||
}
|
||||
f.resolveRespLock.Unlock()
|
||||
|
||||
if f.prefetchExitCh != nil {
|
||||
<-f.prefetchExitCh
|
||||
}
|
||||
}
|
||||
|
||||
// stopPrefetchAndGetData stops the async prefetch and returns prefetched bytes
|
||||
// It also copies prefetched data to the target file
|
||||
func (f *Fetcher) stopPrefetchAndCopyData() int64 {
|
||||
// Signal prefetch to stop (safely)
|
||||
if f.prefetchStopCh != nil {
|
||||
select {
|
||||
case <-f.prefetchStopCh:
|
||||
// Already closed
|
||||
default:
|
||||
close(f.prefetchStopCh)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for prefetch to finish (with timeout)
|
||||
for i := 0; f.prefetchStopCh != nil && i < 1000 && !f.prefetchDone.Load(); i++ {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
f.stopPrefetch()
|
||||
|
||||
prefetched := f.prefetchSize.Load()
|
||||
if prefetched == 0 {
|
||||
@@ -872,12 +892,16 @@ func (f *Fetcher) doStart() error {
|
||||
// For non-range resources, the response will be used directly
|
||||
var prefetchedBytes int64
|
||||
if f.meta.Res.Range {
|
||||
resolveComplete := f.knownSize() > 0 && f.prefetchDone.Load() && f.prefetchErr == nil && f.prefetchSize.Load() >= f.knownSize()
|
||||
// Read the size once and judge completeness by it alone: Resolve may have
|
||||
// written every byte before it sets prefetchDone, and a snapshot that
|
||||
// covers the whole file would otherwise finish without copying it.
|
||||
prefetched := f.prefetchSize.Load()
|
||||
resolveComplete := f.knownSize() > 0 && prefetched >= f.knownSize()
|
||||
if f.resolveFallback.Load() && !resolveComplete {
|
||||
// Keep Resolve downloading until the first Range response has been
|
||||
// validated. The Range request starts at a stable snapshot; a valid
|
||||
// takeover discards any overlap downloaded by Resolve in the meantime.
|
||||
prefetchedBytes = f.prefetchSize.Load()
|
||||
prefetchedBytes = prefetched
|
||||
} else {
|
||||
// Resolve already completed, or no live Resolve fallback remains.
|
||||
prefetchedBytes = f.stopPrefetchAndCopyData()
|
||||
@@ -1627,11 +1651,10 @@ func (f *Fetcher) completeFromResolveFallback(conn *connection) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
for !f.prefetchDone.Load() {
|
||||
if conn.ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
select {
|
||||
case <-f.prefetchExitCh:
|
||||
case <-conn.ctx.Done():
|
||||
return false
|
||||
}
|
||||
knownSize := f.hasKnownSize()
|
||||
if f.prefetchErr != nil || (knownSize && f.prefetchSize.Load() != f.meta.Res.Size) {
|
||||
@@ -2438,14 +2461,7 @@ func (f *Fetcher) Pause() error {
|
||||
}
|
||||
|
||||
// Stop prefetch if running
|
||||
if f.prefetchStopCh != nil {
|
||||
select {
|
||||
case <-f.prefetchStopCh:
|
||||
// Already closed
|
||||
default:
|
||||
close(f.prefetchStopCh)
|
||||
}
|
||||
}
|
||||
f.stopPrefetch()
|
||||
|
||||
// Wait for downloadLoop to exit first (it will call wg.Wait internally)
|
||||
if f.downloadLoopDone != nil {
|
||||
@@ -2455,11 +2471,6 @@ func (f *Fetcher) Pause() error {
|
||||
// Wait for all connection goroutines to stop
|
||||
f.wg.Wait()
|
||||
|
||||
// Wait for prefetch to finish
|
||||
for f.prefetchStopCh != nil && !f.prefetchDone.Load() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// If Pause wins before the first Range response is validated, materialize
|
||||
// the Resolve prefix before discarding its temporary file. The first
|
||||
// connection was allocated after the earlier prefetch snapshot, so advance
|
||||
|
||||
@@ -3261,16 +3261,17 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
}
|
||||
|
||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||
fetcher := downloadReady(httpListener, 4, t)
|
||||
ctl := controller.NewController()
|
||||
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
// Setup must run before Resolve, as in the downloader, so the Resolve
|
||||
// request also goes through the proxy.
|
||||
f := buildFetcher()
|
||||
f.ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
return (&base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
}).ToHandler()
|
||||
}
|
||||
fetcher.Setup(ctl)
|
||||
fetcher := doDownloadReady(f, httpListener, 4, t)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
gohttp "net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
)
|
||||
|
||||
// Start can run after the Resolve prefetch wrote every byte but before it set
|
||||
// prefetchDone. The prefetched body must still be copied to the target.
|
||||
func TestFetcher_StartBeforePrefetchDoneCopiesFullBody(t *testing.T) {
|
||||
payload := []byte("prefetched before Start")
|
||||
server := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
|
||||
gohttp.ServeContent(w, r, "f.txt", time.Time{}, bytes.NewReader(payload))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
f := buildFetcher()
|
||||
f.Meta().Opts = &base.Options{Path: t.TempDir(), Name: "f.txt", Extra: &http.OptsExtra{Connections: 1}}
|
||||
if err := f.Resolve(&base.Request{URL: server.URL + "/f.txt"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for !f.prefetchDone.Load() {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
// Reopen the window between the last write and prefetchDone.
|
||||
f.prefetchDone.Store(false)
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
f.prefetchDone.Store(true)
|
||||
}()
|
||||
|
||||
if err := f.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := os.ReadFile(f.meta.SingleFilepath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("content = %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
gohttp "net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
)
|
||||
|
||||
// A stalled Resolve body must not keep the prefetch goroutine alive after
|
||||
// stopPrefetchAndCopyData returns, or it writes to a file we already removed.
|
||||
func TestFetcher_StopPrefetchUnblocksStalledRead(t *testing.T) {
|
||||
prefix := bytes.Repeat([]byte("p"), 64*1024)
|
||||
stall := make(chan struct{})
|
||||
defer close(stall)
|
||||
|
||||
server := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
|
||||
w.Header().Set(base.HttpHeaderAcceptRanges, base.HttpHeaderBytes)
|
||||
w.Header().Set(base.HttpHeaderContentLength, fmt.Sprintf("%d", len(prefix)*4))
|
||||
w.WriteHeader(gohttp.StatusOK)
|
||||
_, _ = w.Write(prefix)
|
||||
w.(gohttp.Flusher).Flush()
|
||||
select {
|
||||
case <-stall:
|
||||
case <-r.Context().Done():
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
f := buildFetcher()
|
||||
f.Meta().Opts = &base.Options{
|
||||
Path: t.TempDir(),
|
||||
Name: "stalled.data",
|
||||
Extra: &http.OptsExtra{Connections: 1},
|
||||
}
|
||||
if err := f.Resolve(&base.Request{URL: server.URL + "/stalled.data"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for f.prefetchSize.Load() < int64(len(prefix)) {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
f.stopPrefetch()
|
||||
if elapsed := time.Since(start); elapsed > 2*time.Second {
|
||||
t.Fatalf("stopPrefetch took %v, want it to interrupt the stalled read", elapsed)
|
||||
}
|
||||
if !f.prefetchDone.Load() {
|
||||
t.Fatal("prefetch goroutine still running after stopPrefetch")
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class AppRuntimeState {
|
||||
class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
static StartConfig? _defaultStartConfig;
|
||||
Future<void> _preferenceWrites = Future<void>.value();
|
||||
bool _analyticsStarted = false;
|
||||
|
||||
@override
|
||||
Future<AppRuntimeState> build() async {
|
||||
@@ -73,6 +74,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
startConfig
|
||||
..apiEnable = true
|
||||
..mcpEnable = config.api.mcpEnable;
|
||||
await _initAnalytics(config);
|
||||
} else {
|
||||
startConfig
|
||||
..apiEnable = config.api.enable
|
||||
@@ -253,7 +255,8 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
}
|
||||
|
||||
Future<void> _initAnalytics(DownloaderConfig config) async {
|
||||
if (!AnalyticsConfig.isConfigured || !config.extra.analyticsEnabled) return;
|
||||
if (_analyticsStarted || !AnalyticsConfig.isConfigured || !config.extra.analyticsEnabled) return;
|
||||
_analyticsStarted = true;
|
||||
try {
|
||||
final generatedClientId = await Analytics.instance.init(fallbackClientId: config.extra.deviceId);
|
||||
if (generatedClientId != null) {
|
||||
@@ -262,6 +265,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
}
|
||||
unawaited(Analytics.instance.logAppOpen());
|
||||
} catch (error, stackTrace) {
|
||||
_analyticsStarted = false;
|
||||
logger.w('GA4 init failed', error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ Future<DownloaderConfig> loadDownloaderConfig({
|
||||
config = await load();
|
||||
} catch (error, stackTrace) {
|
||||
onLoadError?.call(error, stackTrace);
|
||||
config = DownloaderConfig();
|
||||
config = DownloaderConfig()..extra.analyticsEnabled = false;
|
||||
loaded = false;
|
||||
}
|
||||
final changed = await initDownloaderConfig(config, defaultDownloadDir: defaultDownloadDir);
|
||||
|
||||
@@ -32,8 +32,22 @@ void main() {
|
||||
// The in-memory fallback still gets usable defaults.
|
||||
expect(config.downloadDir, '/downloads');
|
||||
expect(config.categories, hasLength(4));
|
||||
expect(config.extra.analyticsEnabled, isFalse);
|
||||
});
|
||||
|
||||
for (final analyticsEnabled in [false, true]) {
|
||||
test('preserves the saved analytics preference ($analyticsEnabled)', () async {
|
||||
final stored = configWith()..extra.analyticsEnabled = analyticsEnabled;
|
||||
final config = await loadDownloaderConfig(
|
||||
load: () async => stored,
|
||||
save: (_) async {},
|
||||
defaultDownloadDir: () async => '/downloads',
|
||||
);
|
||||
|
||||
expect(config.extra.analyticsEnabled, analyticsEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
test('skips persist when init changed nothing', () async {
|
||||
var saved = 0;
|
||||
await loadDownloaderConfig(
|
||||
|
||||
Reference in new issue
Block a user