Compare commits

..
40 Commits
Author SHA1 Message Date
Levi 76bec67ab2 ci: fix windows runner 2025-10-12 22:45:43 +08:00
Levi a2a9e3dc2a release: v1.8.2 2025-10-12 21:17:40 +08:00
Levi 9a5680fb45 feat: refactor browser extension communication to use IPC (#1130) 2025-10-12 21:17:07 +08:00
Levi 75a8a3a9ac ci: downgrade to Ubuntu 22.04 (#1128) 2025-10-09 21:01:06 +08:00
Levi 3e8c85b945 feat: update GitHub mirrors (#1137) 2025-10-09 20:59:56 +08:00
Levi 90f70d1bf6 feat: isolate TLS fingerprint for each extension lifecycle (#1135) 2025-10-09 20:37:21 +08:00
Levi 20a70d7f91 fix: certain special single-file torrent downloads (#1136) 2025-09-29 22:52:06 +08:00
Levi d33546a4b5 fix: update deleteAll method to accept force parameter (#1127) 2025-09-29 19:41:48 +08:00
Levi 93614d46a8 release: v1.8.1 2025-09-14 10:10:30 +08:00
Levi 04b0e691eb fix: add timeout to server shutdown (#1116) 2025-09-13 21:19:28 +08:00
铺盖崽 7f08fa0b23 ci: update linux runner 24.04 (#1117) 2025-09-13 18:44:00 +08:00
Levi df6a51d23c feat: add notify when new version option (#1115) 2025-09-12 18:32:45 +08:00
Levi 46c379d556 fix: resolve incorrect download path for single file torrent (#1111) 2025-09-10 07:59:10 +08:00
Levi 46cd9f7a3e fix: windows tray disappearing after sleep/wake cycles (#1108) 2025-09-09 22:44:09 +08:00
Levi 5c2f66f4b6 fix: web auth config (#1094) 2025-09-09 18:35:04 +08:00
Krau cfdcb586ee fix: decode URL-encoded filename from contentDisposition header (#1099) 2025-09-04 18:22:30 +08:00
Levi 55e6bdc4f7 release: v1.8.0 2025-08-28 18:43:44 +08:00
Levi b31b42301e feat: update storage directory path for Windows (#1087) 2025-08-28 08:44:18 +08:00
Levi 7ce0d876b0 feat: add environment variables configuration support (#1088) 2025-08-28 08:21:02 +08:00
Levi 2fd2596aa0 ci: update macOS version to macos-14 (#1086) 2025-08-26 10:07:33 +08:00
Local-Gay-Communist e0973e4a34 feat: add german language support (#1084) 2025-08-22 09:26:37 +08:00
Levi 4f7b8a05ad feat: web login page (#1058) 2025-07-16 01:02:25 +08:00
Levi 2927d1ef2b feat(extension): TLS fingerprinting support in xhr (#1054) 2025-07-15 23:54:51 +08:00
Levi 3451f577ab fix: remove invalid characters in HTTP headers (#1053) 2025-07-15 22:34:10 +08:00
Levi 06671be0d8 release: v1.7.1 2025-07-08 09:13:39 +08:00
Thiago Sabaia 8ae9363f0f feat: Translation into Portuguese (Brazil) (#1047) 2025-06-17 09:51:51 +08:00
unifan 5d84445f8f feat: Open in Folder with support for 3rd file manager (#1045) 2025-06-07 11:14:58 +08:00
Levi 7c541e4047 feat: web support relative base path (#1001) 2025-05-15 22:07:26 +08:00
Charles Huang 3262e5a11b fix: do not add external tracker to a private torrent (#1011) 2025-04-23 22:53:15 +08:00
Charles Huang ff43a373cd fix: json syntax error in bt config (#1014) 2025-04-23 13:48:46 +08:00
Levi 7394d948e4 ci: workaround for app_indicator_new is deprecated (#986) 2025-04-09 22:20:16 +08:00
Levi 5afb3aa43b ci: fix go 1.24 code coverage (#988) 2025-04-09 22:19:50 +08:00
Levi 7705af6e68 release: v1.7.0 2025-04-08 22:22:09 +08:00
Levi 684e060ef3 chore: upgrade go to 1.24 (#984) 2025-04-08 22:18:18 +08:00
Levi 0c67792441 ci: build separate macos packages (#967) 2025-04-08 19:43:30 +08:00
Levi b0ef2c2a87 fix: bt seed cause crash (#985) 2025-04-08 18:38:17 +08:00
mak7im01 1977c2a0fc feat: update ru_ru.dart (#982) 2025-04-08 09:21:29 +08:00
Levi d289708f9e feat: use batch download api for create multitasks (#978) 2025-04-05 22:57:17 +08:00
Levi cd5fd45667 fix: resolve http non encode attachment filename (#975) 2025-04-05 22:37:19 +08:00
Levi 50bc9ab129 feat: improve http download strategy to prevent speed drops near completion (#969) 2025-04-05 22:20:35 +08:00
80 changed files with 4054 additions and 689 deletions

No files matched your search

+11 -3
View File
@@ -51,14 +51,22 @@ template: |
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
</tr> </tr>
<tr> <tr>
<td>🍎 MacOS</td> <td rowspan="3">🍎 MacOS</td>
<td><code>DMG</code></td> <td rowspan="3"><code>DMG</code></td>
<td>universal</td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
</tr> </tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
</tr>
<tr> <tr>
<td rowspan="6">🐧 Linux</td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td <td><code>Flathub</code></td>
<td>amd64</td> <td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td> <td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr> </tr>
+55 -39
View File
@@ -13,7 +13,7 @@ on:
default: "false" default: "false"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.24.5" FLUTTER_VERSION: "3.24.5"
permissions: permissions:
@@ -34,7 +34,7 @@ jobs:
build-windows: build-windows:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
runs-on: windows-latest runs-on: windows-2022
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -139,9 +139,9 @@ jobs:
upload_url: ${{ needs.get-release.outputs.upload_url }} upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/windows/Output/* asset_path: ui/flutter/build/windows/Output/*
overwrite: true overwrite: true
build-macos-arm64-lib: build-macos-amd64-lib:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-latest runs-on: macos-13
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -157,15 +157,18 @@ jobs:
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: macos-arm64-lib name: macos-amd64-lib
path: | path: |
libgopeed.dylib libgopeed.dylib
host host
updater updater
build-macos: build-macos:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-13 runs-on: macos-latest
needs: [get-release, build-macos-arm64-lib] strategy:
matrix:
channel: [ arm64, amd64, universal ]
needs: [get-release, build-macos-amd64-lib]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -183,34 +186,46 @@ jobs:
npm install -g appdmg npm install -g appdmg
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: macos-arm64-lib name: macos-amd64-lib
path: ui/flutter/lib-arm64 path: ui/flutter/lib-amd64
- if: ${{ matrix.channel == 'arm64' }}
name: Build Arm64 Libraries
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
- if: ${{ matrix.channel == 'amd64' }}
name: Build Amd64 Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/libgopeed.dylib
cp ui/flutter/lib-amd64/host ui/flutter/assets/exec/host
cp ui/flutter/lib-amd64/updater ui/flutter/assets/exec/updater
- if: ${{ matrix.channel == 'universal' }}
name: Build Universal Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
# arm64 lib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/.temp/libgopeed.dylib.arm64 github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/.temp/host.arm64 github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/.temp/updater.arm64 github.com/GopeedLab/gopeed/cmd/updater
# amd64 lib
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.amd64
cp ui/flutter/lib-amd64/host ui/flutter/.temp/host.amd64
cp ui/flutter/lib-amd64/updater ui/flutter/.temp/updater.amd64
# universal lib
lipo -create -output ui/flutter/macos/Frameworks/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.arm64 ui/flutter/.temp/libgopeed.dylib.amd64
lipo -create -output ui/flutter/assets/exec/host ui/flutter/.temp/host.arm64 ui/flutter/.temp/host.amd64
lipo -create -output ui/flutter/assets/exec/updater ui/flutter/.temp/updater.arm64 ui/flutter/.temp/updater.amd64
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
PROJECT_DIR=$(pwd) cd ui/flutter
# amd64 lib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/updater github.com/GopeedLab/gopeed/cmd/updater
# universal binary
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
cp ui/flutter/lib-amd64/host ui/flutter/macos/Frameworks/amd64-host
cp ui/flutter/lib-arm64/host ui/flutter/macos/Frameworks/arm64-host
cp ui/flutter/lib-amd64/updater ui/flutter/macos/Frameworks/amd64-updater
cp ui/flutter/lib-arm64/updater ui/flutter/macos/Frameworks/arm64-updater
cd ui/flutter/macos/Frameworks
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
lipo -create -output host amd64-host arm64-host
lipo -create -output updater amd64-updater arm64-updater
rm -rf amd64-lib arm64-lib amd64-host arm64-host amd64-updater arm64-updater
mv host ../../assets/exec/host
cd $PROJECT_DIR/ui/flutter
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg" flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
cd build/macos/Build/Products/Release cd build/macos/Build/Products/Release
cat>appdmg.json<<EOF cat>appdmg.json<<EOF
@@ -225,6 +240,13 @@ jobs:
EOF EOF
mkdir dist mkdir dist
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
if [[ "${{ matrix.channel }}" == "arm64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-arm64.dmg
fi
if [[ "${{ matrix.channel }}" == "amd64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-amd64.dmg
fi
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -243,12 +265,7 @@ jobs:
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- if: matrix.os == 'ubuntu-22.04' - uses: subosito/flutter-action@v2
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- if: matrix.os == 'ubuntu-22.04-arm'
uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
channel: master channel: master
@@ -260,7 +277,6 @@ jobs:
sudo apt install -y libfuse2 locate sudo apt install -y libfuse2 locate
arch=$(uname -m) arch=$(uname -m)
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage" wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
chmod +x appimagetool chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/ sudo mv appimagetool /usr/local/bin/
- name: Build - name: Build
@@ -486,7 +502,7 @@ jobs:
overwrite: true overwrite: true
build-ios: build-ios:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
runs-on: macos-latest runs-on: macos-14
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile" - "Dockerfile"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
permissions: permissions:
contents: write contents: write
+14 -8
View File
@@ -27,7 +27,7 @@ on:
- "go.sum" - "go.sum"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.24.5" FLUTTER_VERSION: "3.24.5"
jobs: jobs:
@@ -53,7 +53,13 @@ jobs:
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests. - name: Run Unit tests.
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/) -v -covermode=count -coverprofile=coverage.txt run: |
# Get packages for testing and coverage (including cmd/web for flags_test.go)
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
# Filter out main.go from coverage report to avoid lowering coverage percentage
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
mv coverage_filtered.txt coverage.txt
- uses: codecov/codecov-action@v4 - uses: codecov/codecov-action@v4
with: with:
files: ./coverage.txt files: ./coverage.txt
@@ -63,8 +69,8 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ] os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -96,8 +102,8 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ macos-latest, ubuntu-latest ] os: [macos-14, ubuntu-latest]
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -129,7 +135,7 @@ jobs:
build-web: build-web:
name: Build web check name: Build web check
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -149,7 +155,7 @@ jobs:
build-docker: build-docker:
name: Build docker check name: Build docker check
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ test ] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
+10 -2
View File
@@ -35,11 +35,19 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr> </tr>
<tr> <tr>
<td>🍎 MacOS</td> <td rowspan="3">🍎 MacOS</td>
<td><code>DMG</code></td> <td rowspan="3"><code>DMG</code></td>
<td>universal</td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr> </tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr> <tr>
<td rowspan="6">🐧 Linux</td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td> <td><code>Flathub</code></td>
+10 -2
View File
@@ -35,11 +35,19 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr> </tr>
<tr> <tr>
<td>🍎 MacOS</td> <td rowspan="3">🍎 MacOS</td>
<td><code>DMG</code></td> <td rowspan="3"><code>DMG</code></td>
<td>universal</td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr> </tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr> <tr>
<td rowspan="6">🐧 Linux</td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td> <td><code>Flathub</code></td>
+10 -2
View File
@@ -35,11 +35,19 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr> </tr>
<tr> <tr>
<td>🍎 MacOS</td> <td rowspan="3">🍎 MacOS</td>
<td><code>DMG</code></td> <td rowspan="3"><code>DMG</code></td>
<td>universal</td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr> </tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr> <tr>
<td rowspan="6">🐧 Linux</td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td> <td><code>Flathub</code></td>
+10 -2
View File
@@ -35,11 +35,19 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr> </tr>
<tr> <tr>
<td>🍎 MacOS</td> <td rowspan="3">🍎 MacOS</td>
<td><code>DMG</code></td> <td rowspan="3"><code>DMG</code></td>
<td>universal</td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr> </tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr> <tr>
<td rowspan="6">🐧 Linux</td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td> <td><code>Flathub</code></td>
+10 -2
View File
@@ -35,11 +35,19 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr> </tr>
<tr> <tr>
<td>🍎 MacOS</td> <td rowspan="3">🍎 MacOS</td>
<td><code>DMG</code></td> <td rowspan="3"><code>DMG</code></td>
<td>universal</td> <td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr> </tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr> <tr>
<td rowspan="6">🐧 Linux</td> <td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td> <td><code>Flathub</code></td>
+1
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
+19
View File
@@ -0,0 +1,19 @@
//go:build !windows
// +build !windows
package main
import (
"net"
"os"
"path/filepath"
)
func Dial() (net.Conn, error) {
// Get binary path
exe, err := os.Executable()
if err != nil {
return nil, err
}
return net.Dial("unix", filepath.Join(filepath.Dir(exe), "gopeed_host.sock"))
}
+11
View File
@@ -0,0 +1,11 @@
package main
import (
"net"
"github.com/Microsoft/go-winio"
)
func Dial() (net.Conn, error) {
return winio.DialPipe(`\\.\pipe\gopeed_host`, nil)
}
+69 -24
View File
@@ -1,22 +1,24 @@
package main package main
import ( import (
"bytes"
"context"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net"
"net/http"
"os" "os"
"strings" "time"
"github.com/pkg/browser" "github.com/pkg/browser"
"github.com/shirou/gopsutil/v4/process"
) )
const identifier = "gopeed"
type Message struct { type Message struct {
Method string `json:"method"` Method string `json:"method"`
Meta map[string]any `json:"meta"`
Params json.RawMessage `json:"params"` Params json.RawMessage `json:"params"`
} }
@@ -26,31 +28,74 @@ type Response struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
} }
var apiMap = map[string]func(params json.RawMessage) (data any, err error){ func check() (data bool, err error) {
"ping": func(params json.RawMessage) (data any, err error) { conn, err := Dial()
processes, err := process.Processes() if err != nil {
if err != nil { return false, err
return false, err }
} defer conn.Close()
return true, nil
}
for _, p := range processes { func wakeup(hidden bool) error {
name, err := p.Name() running, _ := check()
if err != nil { if running {
continue return nil
} }
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) { uri := "gopeed:"
return true, nil if hidden {
} uri = uri + "?hidden=true"
}
if err := browser.OpenURL(uri); err != nil {
return err
}
for i := 0; i < 10; i++ {
if ok, _ := check(); ok {
return nil
} }
return false, nil time.Sleep(1 * time.Second)
}
return fmt.Errorf("start gopeed failed")
}
var apiMap = map[string]func(message *Message) (data any, err error){
"ping": func(message *Message) (data any, err error) {
return check()
}, },
"create": func(params json.RawMessage) (data any, err error) { "create": func(message *Message) (data any, err error) {
var strParams string buf, err := message.Params.MarshalJSON()
if err = json.Unmarshal(params, &strParams); err != nil { if err != nil {
return return
} }
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
silent := false
if v, ok := message.Meta["silent"]; ok {
silent, _ = v.(bool)
}
if err := wakeup(silent); err != nil {
return nil, err
}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return Dial()
},
},
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("POST", "http://127.0.0.1/create", bytes.NewBuffer(buf))
if err != nil {
return
}
if message.Meta != nil {
metaJson, _ := json.Marshal(message.Meta)
req.Header.Set("X-Gopeed-Host-Meta", string(metaJson))
}
_, err = client.Do(req)
return return
}, },
} }
@@ -88,7 +133,7 @@ func main() {
var data any var data any
var err error var err error
if handler, ok := apiMap[message.Method]; ok { if handler, ok := apiMap[message.Method]; ok {
data, err = handler(message.Params) data, err = handler(&message)
} else { } else {
err = errors.New("Unknown method: " + message.Method) err = errors.New("Unknown method: " + message.Method)
} }
+5 -6
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath"
"syscall" "syscall"
"time" "time"
@@ -19,6 +18,7 @@ func main() {
pid := flag.Int("pid", 0, "PID of the process to update") pid := flag.Int("pid", 0, "PID of the process to update")
updateChannel := flag.String("channel", "", "Update channel") updateChannel := flag.String("channel", "", "Update channel")
packagePath := flag.String("asset", "", "Path to the package asset") packagePath := flag.String("asset", "", "Path to the package asset")
exeDir := flag.String("exeDir", "", "Directory of the entry executable")
logPath := flag.String("log", "", "Log file path") logPath := flag.String("log", "", "Log file path")
flag.Parse() flag.Parse()
@@ -43,7 +43,7 @@ func main() {
restart bool restart bool
err error err error
) )
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil { if restart, err = update(*pid, *updateChannel, *packagePath, *exeDir); err != nil {
log.Printf("Update failed: %v\n", err) log.Printf("Update failed: %v\n", err)
os.Exit(1) os.Exit(1)
} }
@@ -61,7 +61,7 @@ func main() {
os.Exit(0) os.Exit(0)
} }
func update(pid int, updateChannel, packagePath string) (restart bool, err error) { func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, err error) {
killSignalChan := make(chan any, 1) killSignalChan := make(chan any, 1)
go func() { go func() {
@@ -76,9 +76,8 @@ func update(pid int, updateChannel, packagePath string) (restart bool, err error
} }
}() }()
appDir := filepath.Dir(os.Args[0]) log.Printf("Updating process updateChannel=%s packagePath=%s exeDir=%s\n", updateChannel, packagePath, exeDir)
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir) if restart, err = install(killSignalChan, updateChannel, packagePath, exeDir); err != nil {
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
return false, errors.Wrap(err, "failed to install package") return false, errors.Wrap(err, "failed to install package")
} }
+9 -5
View File
@@ -25,16 +25,20 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
mountPoint := "" mountPoint := ""
for _, line := range strings.Split(string(output), "\n") { for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "/Volumes/") { if strings.Contains(line, "/Volumes/") {
fields := strings.Fields(line) // Find the /Volumes/ path in the line
if len(fields) > 2 { // hdiutil output format: /dev/disk4s1 Apple_HFS /Volumes/Gopeed
mountPoint = fields[len(fields)-1] // or with sequence number: /dev/disk4s1 Apple_HFS /Volumes/Gopeed 1
idx := strings.Index(line, "/Volumes/")
if idx != -1 {
// Extract everything from /Volumes/ onwards and trim whitespace
mountPoint = strings.TrimSpace(line[idx:])
break break
} }
} }
} }
if mountPoint == "" { if mountPoint == "" {
return fmt.Errorf("failed to get mount point") return fmt.Errorf("failed to get mount point from hdiutil output: %s", string(output))
} }
// Detach the mounted DMG // Detach the mounted DMG
@@ -45,7 +49,7 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
return err return err
} }
if len(matches) == 0 { if len(matches) == 0 {
return fmt.Errorf("no .app found in dmg") return fmt.Errorf("no .app found in dmg, mountPoint: %s", mountPoint)
} }
killSignalChan <- nil killSignalChan <- nil
+166 -51
View File
@@ -3,19 +3,23 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strconv"
"strings"
"github.com/GopeedLab/gopeed/pkg/base"
) )
type args struct { type args struct {
Address *string `json:"address"` Address *string `json:"address"`
Port *int `json:"port"` Port *int `json:"port"`
Username *string `json:"username"` Username *string `json:"username"`
Password *string `json:"password"` Password *string `json:"password"`
ApiToken *string `json:"apiToken"` ApiToken *string `json:"apiToken"`
StorageDir *string `json:"storageDir"` StorageDir *string `json:"storageDir"`
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
// DownloadConfig when the first time to start the server, it will be configured as initial value // DownloadConfig when the first time to start the server, it will be configured as initial value
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"` DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
@@ -23,62 +27,173 @@ type args struct {
} }
func parse() *args { func parse() *args {
var cliArgs args cfg := &args{}
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port") cliConfig := loadCliArgs()
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username") loadConfigFile(cfg, *cliConfig.configPath)
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd") loadEnvVars(cfg)
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication") // override with non-default command line arguments
cliArgs.StorageDir = flag.String("d", "", "Storage directory") overrideWithCliArgs(cfg, cliConfig)
cliArgs.configPath = flag.String("c", "./config.json", "Config file path") // set default values
setDefaults(cfg, cliConfig)
return cfg
}
// loadCliArgs parses command line arguments and returns initial config
func loadCliArgs() *args {
cfg := &args{}
cfg.Address = flag.String("A", "0.0.0.0", "Bind Address")
cfg.Port = flag.Int("P", 9999, "Bind Port")
cfg.Username = flag.String("u", "gopeed", "Web Authentication Username")
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
cfg.StorageDir = flag.String("d", "", "Storage directory")
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
cfg.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse() flag.Parse()
// args priority: config file > cli args // Parse white download directories from comma-separated string
cfgArgs := loadConfig(*cliArgs.configPath) if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
if cfgArgs.Address == nil { dirs := strings.Split(*whiteDownloadDirs, ",")
cfgArgs.Address = cliArgs.Address for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
cfg.WhiteDownloadDirs = dirs
} }
if cfgArgs.Port == nil {
cfgArgs.Port = cliArgs.Port return cfg
}
if cfgArgs.Username == nil {
cfgArgs.Username = cliArgs.Username
}
if cfgArgs.Password == nil {
cfgArgs.Password = cliArgs.Password
}
if cfgArgs.ApiToken == nil {
cfgArgs.ApiToken = cliArgs.ApiToken
}
if cfgArgs.StorageDir == nil {
cfgArgs.StorageDir = cliArgs.StorageDir
}
return cfgArgs
} }
func loadConfig(path string) *args { // overrideWithCliArgs overrides config with non-empty command line arguments
var args args func overrideWithCliArgs(cfg *args, cliConfig *args) {
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "A":
cfg.Address = cliConfig.Address
case "P":
cfg.Port = cliConfig.Port
case "u":
cfg.Username = cliConfig.Username
case "p":
cfg.Password = cliConfig.Password
case "T":
cfg.ApiToken = cliConfig.ApiToken
case "d":
cfg.StorageDir = cliConfig.StorageDir
case "w":
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
case "c":
cfg.configPath = cliConfig.configPath
}
})
}
if !filepath.IsAbs(path) { // setDefaults sets default values for any unset configuration fields
func setDefaults(cfg *args, cliConfig *args) {
if cfg.Address == nil {
cfg.Address = cliConfig.Address
}
if cfg.Port == nil {
cfg.Port = cliConfig.Port
}
if cfg.Username == nil {
cfg.Username = cliConfig.Username
}
if cfg.Password == nil {
cfg.Password = cliConfig.Password
}
if cfg.ApiToken == nil {
cfg.ApiToken = cliConfig.ApiToken
}
if cfg.StorageDir == nil {
cfg.StorageDir = cliConfig.StorageDir
}
}
// loadConfigFile loads configuration from file
func loadConfigFile(cfg *args, configPath string) {
if !filepath.IsAbs(configPath) {
dir, err := os.Getwd() dir, err := os.Getwd()
if err != nil { if err != nil {
fmt.Println("config dir get failed, reason:" + err.Error()) return
return &args
} }
path = filepath.Join(dir, path) configPath = filepath.Join(dir, configPath)
} }
file, err := os.ReadFile(path)
file, err := os.ReadFile(configPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return &args return
} }
fmt.Println("config file read failed, reason:" + err.Error()) return
return &args
} }
if err = json.Unmarshal(file, &args); err != nil {
fmt.Println("config file parse failed, reason:" + err.Error()) if err = json.Unmarshal(file, cfg); err != nil {
return &args return
}
}
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
func loadEnvVars(cfg *args) {
v := reflect.ValueOf(cfg).Elem()
t := reflect.TypeOf(cfg).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
// Get json tag as environment variable suffix
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Remove options like omitempty
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
jsonTag = jsonTag[:commaIdx]
}
// Convert to uppercase and add GOPEED_ prefix
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
envValue := os.Getenv(envKey)
if envValue == "" {
continue
}
// Set value based on field type
if field.Kind() == reflect.Ptr {
if field.IsNil() {
// Create new pointer instance
newVal := reflect.New(field.Type().Elem())
field.Set(newVal)
}
switch field.Type().Elem().Kind() {
case reflect.String:
field.Elem().SetString(envValue)
case reflect.Int:
if intVal, err := strconv.Atoi(envValue); err == nil {
field.Elem().SetInt(int64(intVal))
}
default:
// For complex types like DownloadConfig, try JSON unmarshaling
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
var config base.DownloaderStoreConfig
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
field.Set(reflect.ValueOf(&config))
}
}
}
} else if field.Kind() == reflect.Slice {
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
if field.Type().Elem().Kind() == reflect.String {
dirs := strings.Split(envValue, ",")
for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
field.Set(reflect.ValueOf(dirs))
}
}
} }
fmt.Printf("config file loaded: %s\n", path)
return &args
} }
File diff suppressed because it is too large. Load diff
+19 -17
View File
@@ -6,11 +6,12 @@ package main
import ( import (
"embed" "embed"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
) )
//go:embed dist/* //go:embed dist/*
@@ -23,36 +24,37 @@ func main() {
} }
args := parse() args := parse()
var webBasicAuth *model.WebBasicAuth var webAuth *model.WebAuth
if isNotBlank(args.Username) && isNotBlank(args.Password) { if isNotBlank(args.Username) && isNotBlank(args.Password) {
webBasicAuth = &model.WebBasicAuth{ webAuth = &model.WebAuth{
Username: *args.Username, Username: *args.Username,
Password: *args.Password, Password: *args.Password,
} }
} }
var dir string var storageDir string
if args.StorageDir != nil && *args.StorageDir != "" { if args.StorageDir != nil && *args.StorageDir != "" {
dir = *args.StorageDir storageDir = *args.StorageDir
} else { } else {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
panic(err) panic(err)
} }
dir = filepath.Dir(exe) storageDir = filepath.Join(filepath.Dir(exe), "storage")
} }
cfg := &model.StartConfig{ cfg := &model.StartConfig{
Network: "tcp", Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port), Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt, Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"), StorageDir: storageDir,
ApiToken: *args.ApiToken, WhiteDownloadDirs: args.WhiteDownloadDirs,
DownloadConfig: args.DownloadConfig, ApiToken: *args.ApiToken,
ProductionMode: true, DownloadConfig: args.DownloadConfig,
WebEnable: true, ProductionMode: true,
WebFS: sub, WebEnable: true,
WebBasicAuth: webBasicAuth, WebFS: sub,
WebAuth: webAuth,
} }
cmd.Start(cfg) cmd.Start(cfg)
} }
+22 -9
View File
@@ -13,6 +13,7 @@ require (
github.com/go-git/go-git/v5 v5.8.1 github.com/go-git/go-git/v5 v5.8.1
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/imroc/req/v3 v3.52.2
github.com/matoous/go-nanoid/v2 v2.0.0 github.com/matoous/go-nanoid/v2 v2.0.0
github.com/mattn/go-ieproxy v0.0.12 github.com/mattn/go-ieproxy v0.0.12
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
@@ -21,17 +22,29 @@ require (
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
go.etcd.io/bbolt v1.3.11 go.etcd.io/bbolt v1.3.11
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
golang.org/x/sync v0.9.0 golang.org/x/sync v0.13.0
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/ebitengine/purego v0.8.1 // indirect github.com/ebitengine/purego v0.8.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // 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/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
) )
require ( require (
@@ -61,7 +74,7 @@ require (
github.com/bits-and-blooms/bitset v1.15.0 // indirect github.com/bits-and-blooms/bitset v1.15.0 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
@@ -77,7 +90,7 @@ require (
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // 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-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
@@ -122,13 +135,13 @@ require (
go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect
golang.org/x/crypto v0.29.0 // indirect golang.org/x/crypto v0.37.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.31.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.20.0 // indirect golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.8.0 // indirect golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.27.0 // indirect golang.org/x/tools v0.32.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
modernc.org/libc v1.61.0 // indirect modernc.org/libc v1.61.0 // indirect
+61 -24
View File
@@ -94,6 +94,8 @@ github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic= github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI= github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8= github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -122,8 +124,9 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -196,6 +199,8 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -229,12 +234,12 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -250,6 +255,11 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@@ -262,6 +272,10 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.52.2 h1:xJocr1aIv0a2K9knfBQ4JnZHk+kWTITdjf0mgDg229I=
github.com/imroc/req/v3 v3.52.2/go.mod h1:dBGsDloOSZJcFs6PnTjZXYBJK70OXbZpizHBLNqcH2k=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -277,6 +291,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -331,7 +347,11 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -380,6 +400,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -404,7 +426,13 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE= github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM= github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -452,8 +480,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
@@ -471,6 +500,8 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g= github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI= github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -487,8 +518,12 @@ go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZk
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -497,8 +532,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
@@ -512,8 +547,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -535,8 +570,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -550,8 +585,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -590,17 +625,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -609,8 +642,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -624,8 +657,8 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -646,6 +679,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -667,6 +702,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
+4 -2
View File
@@ -24,8 +24,10 @@ type DefaultFileController struct {
func NewController() *Controller { func NewController() *Controller {
return &Controller{ return &Controller{
GetConfig: func(v any) {}, GetConfig: func(v any) {},
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil }, GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) {
return requestProxy.ToHandler()
},
FileController: &DefaultFileController{}, FileController: &DefaultFileController{},
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ type config struct {
ListenPort int `json:"listenPort"` ListenPort int `json:"listenPort"`
Trackers []string `json:"trackers"` Trackers []string `json:"trackers"`
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped. // SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
SeedKeep bool `json:"seedKeep` SeedKeep bool `json:"seedKeep"`
// SeedRatio is the ratio of uploaded data to downloaded data to seed. // SeedRatio is the ratio of uploaded data to downloaded data to seed.
SeedRatio float64 `json:"seedRatio"` SeedRatio float64 `json:"seedRatio"`
// SeedTime is the time in seconds to seed after downloading is complete. // SeedTime is the time in seconds to seed after downloading is complete.
+41 -27
View File
@@ -4,13 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"io" "io"
"net/url" "net/url"
"os" "os"
@@ -19,6 +12,14 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
) )
var ( var (
@@ -100,9 +101,6 @@ func (f *Fetcher) initClient() (err error) {
} }
func (f *Fetcher) Resolve(req *base.Request) error { func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
f.meta.Req = req f.meta.Req = req
if err := f.addTorrent(req, false); err != nil { if err := f.addTorrent(req, false); err != nil {
return err return err
@@ -355,10 +353,14 @@ func (f *Fetcher) WaitUpload() (err error) {
} }
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) { func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return
}
if err = f.initClient(); err != nil { if err = f.initClient(); err != nil {
return return
} }
schema := util.ParseSchema(req.URL) schema := util.ParseSchema(req.URL)
privateTorrent := false
if schema == "MAGNET" { if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(req.URL) f.torrent, err = client.AddMagnet(req.URL)
} else { } else {
@@ -388,34 +390,46 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
if err != nil && !strings.Contains(err.Error(), "expected EOF") { if err != nil && !strings.Contains(err.Error(), "expected EOF") {
return err return err
} }
info, er := metaInfo.UnmarshalInfo()
if er != nil {
return er
}
if info.Private != nil && *info.Private {
privateTorrent = true
}
f.torrent, err = client.AddTorrent(metaInfo) f.torrent, err = client.AddTorrent(metaInfo)
} }
if err != nil { if err != nil {
return return
} }
// use map to deduplicate // Do not add external tracker to a private torrent.
trackers := make(map[string]bool) if !privateTorrent {
if req.Extra != nil { // use map to deduplicate
extra := req.Extra.(*bt.ReqExtra) trackers := make(map[string]bool)
if len(extra.Trackers) > 0 { if req.Extra != nil {
for _, tracker := range extra.Trackers { extra := req.Extra.(*bt.ReqExtra)
if len(extra.Trackers) > 0 {
for _, tracker := range extra.Trackers {
trackers[tracker] = true
}
}
}
if len(f.config.Trackers) > 0 {
for _, tracker := range f.config.Trackers {
trackers[tracker] = true trackers[tracker] = true
} }
} }
} if len(trackers) > 0 {
if len(f.config.Trackers) > 0 { announceList := make([][]string, 0)
for _, tracker := range f.config.Trackers { for tracker := range trackers {
trackers[tracker] = true announceList = append(announceList, []string{tracker})
}
f.torrent.AddTrackers(announceList)
} }
} }
if len(trackers) > 0 {
announceList := make([][]string, 0)
for tracker := range trackers {
announceList = append(announceList, []string{tracker})
}
f.torrent.AddTrackers(announceList)
}
<-f.torrent.GotInfo() <-f.torrent.GotInfo()
f.torrentReady.Store(true) f.torrentReady.Store(true)
+6 -5
View File
@@ -3,17 +3,18 @@ package bt
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"testing" "testing"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
) )
func TestFetcher_Resolve_Torrent(t *testing.T) { func TestFetcher_Resolve_Torrent(t *testing.T) {
+3 -2
View File
@@ -5,11 +5,12 @@ package bt
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/anacrolix/torrent/storage"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"github.com/anacrolix/torrent/storage"
"github.com/anacrolix/missinggo/v2" "github.com/anacrolix/missinggo/v2"
"github.com/anacrolix/torrent/common" "github.com/anacrolix/torrent/common"
@@ -34,7 +35,7 @@ func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
if opts.FilePathMaker == nil { if opts.FilePathMaker == nil {
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string { opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
var parts []string var parts []string
if opts.Info.Name != metainfo.NoName { if opts.Info.Length == 0 || opts.File.Path == nil {
parts = append(parts, opts.Info.Name) parts = append(parts, opts.Info.Name)
} }
return filepath.Join(append(parts, opts.File.Path...)...) return filepath.Join(append(parts, opts.File.Path...)...)
File diff suppressed because it is too large. Load diff
+52
View File
@@ -47,6 +47,16 @@ func TestFetcher_Resolve(t *testing.T) {
}, },
}, },
}, t) }, t)
testResolve(test.StartTestCustomServer, "no-encode", &base.Resource{
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: "测试.zip",
Size: test.BuildSize,
},
},
}, t)
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{ testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
Size: 0, Size: 0,
Range: false, Range: false,
@@ -95,6 +105,21 @@ func TestFetcher_ResolveWithHostHeader(t *testing.T) {
} }
} }
func TestFetcher_ResolveWithInvalidHeader(t *testing.T) {
fetcher := buildFetcher()
err := fetcher.Resolve(&base.Request{
URL: "https://bing.com",
Extra: &http.ReqExtra{
Header: map[string]string{
"Referer": "\rtest",
},
},
})
if err != nil {
t.Errorf("Resolve() got = %v, want nil", err)
}
}
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) { func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
listener := startTestServer() listener := startTestServer()
defer listener.Close() defer listener.Close()
@@ -246,6 +271,33 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
} }
} }
func TestFetcher_Stats(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
fetcher := doDownloadReady(buildConfigFetcher(config{
Connections: 16,
}), listener, 0, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
stats := fetcher.Stats().(*http.Stats)
if len(stats.Connections) != 16 {
t.Errorf("Stats() got = %v, want %v", len(stats.Connections), 16)
}
totalDownloaded := int64(0)
for _, conn := range stats.Connections {
totalDownloaded += conn.Downloaded
}
if totalDownloaded != test.BuildSize {
t.Errorf("Stats() got = %v, want %v", totalDownloaded, test.BuildSize)
}
}
func TestFetcherManager_ParseName(t *testing.T) { func TestFetcherManager_ParseName(t *testing.T) {
type args struct { type args struct {
u string u string
+11
View File
@@ -102,6 +102,17 @@ func StartTestCustomServer() net.Listener {
defer file.Close() defer file.Close()
io.Copy(writer, file) io.Copy(writer, file)
}) })
mux.HandleFunc("/no-encode", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Disposition", "attachment; filename=测试.zip")
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux return mux
}) })
} }
+10
View File
@@ -163,6 +163,16 @@ func ParseOptsExtra[E any](opts *Options) error {
return nil return nil
} }
type CreateTaskBatch struct {
Reqs []*CreateTaskBatchItem `json:"reqs"`
Opts *Options `json:"opts"`
}
type CreateTaskBatchItem struct {
Req *Request `json:"req"`
Opts *Options `json:"opts"`
}
// DownloaderStoreConfig is the config that can restore the downloader. // DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct { type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
+38 -30
View File
@@ -2,15 +2,6 @@ package download
import ( import (
"errors" "errors"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"math" "math"
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
@@ -22,6 +13,16 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
) )
const ( const (
@@ -148,6 +149,7 @@ func (d *Downloader) Setup() error {
tasks = append(tasks[:i], tasks[i+1:]...) tasks = append(tasks[:i], tasks[i+1:]...)
continue continue
} }
d.assignFetcherManager(task)
initTask(task) initTask(task)
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError { if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
task.Status = base.DownloadStatusPause task.Status = base.DownloadStatusPause
@@ -358,10 +360,14 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
return d.doCreate(fetcher, opts) return d.doCreate(fetcher, opts)
} }
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) { func (d *Downloader) CreateDirectBatch(req *base.CreateTaskBatch) (taskId []string, err error) {
taskIds := make([]string, 0) taskIds := make([]string, 0)
for _, req := range reqs { for _, ir := range req.Reqs {
taskId, err := d.CreateDirect(req, opts.Clone()) opts := ir.Opts
if opts == nil {
opts = req.Opts
}
taskId, err := d.CreateDirect(ir.Req, opts.Clone())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -527,7 +533,7 @@ func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) { func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
if filter == nil || filter.IsEmpty() { if filter == nil || filter.IsEmpty() {
return d.deleteAll() return d.deleteAll(force)
} }
deleteTasks := d.GetTasksByFilter(filter) deleteTasks := d.GetTasksByFilter(filter)
@@ -572,17 +578,21 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
return return
} }
func (d *Downloader) deleteAll() (err error) { func (d *Downloader) deleteAll(force bool) (err error) {
var deleteTasksTemp []*Task
func() { func() {
d.lock.Lock() d.lock.Lock()
defer d.lock.Unlock() defer d.lock.Unlock()
for _, task := range d.tasks {
deleteTasksTemp = append(deleteTasksTemp, task)
}
d.tasks = make([]*Task, 0) d.tasks = make([]*Task, 0)
d.waitTasks = make([]*Task, 0) d.waitTasks = make([]*Task, 0)
}() }()
for _, task := range d.tasks { for _, task := range deleteTasksTemp {
if err = d.doDelete(task, true); err != nil { if err = d.doDelete(task, force); err != nil {
return return
} }
} }
@@ -892,18 +902,7 @@ func (d *Downloader) restoreTask(task *Task) error {
} }
func (d *Downloader) restoreFetcher(task *Task) error { func (d *Downloader) restoreFetcher(task *Task) error {
var fm fetcher.FetcherManager v, f := task.fetcherManager.Restore()
for _, f := range d.cfg.FetchManagers {
if f.Name() == task.Protocol {
fm = f
break
}
}
if fm == nil {
return ErrUnSupportedProtocol
}
task.fetcherManager = fm
v, f := fm.Restore()
if v != nil { if v != nil {
err := d.storage.Pop(bucketSave, task.ID, v) err := d.storage.Pop(bucketSave, task.ID, v)
if err != nil { if err != nil {
@@ -943,9 +942,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
} }
// if enable white download directory, check if the download directory is in the white list // if enable white download directory, check if the download directory is in the white list
if len(d.cfg.DownloadDirWhiteList) > 0 { if len(d.cfg.WhiteDownloadDirs) > 0 {
inWhiteList := false inWhiteList := false
for _, dir := range d.cfg.DownloadDirWhiteList { for _, dir := range d.cfg.WhiteDownloadDirs {
if match, err := filepath.Match(dir, opts.Path); match && err == nil { if match, err := filepath.Match(dir, opts.Path); match && err == nil {
inWhiteList = true inWhiteList = true
break break
@@ -1145,6 +1144,15 @@ func logPanic(logDir string) {
debug.SetCrashOutput(f, debug.CrashOptions{}) debug.SetCrashOutput(f, debug.CrashOptions{})
} }
func (d *Downloader) assignFetcherManager(task *Task) error {
fm, err := d.parseFm(task.Meta.Req.URL)
if err != nil {
return err
}
task.fetcherManager = fm
return nil
}
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) { func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
fm, err := d.parseFm(url) fm, err := d.parseFm(url)
if err != nil { if err != nil {
+25 -15
View File
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
defer listener.Close() defer listener.Close()
downloader := NewDownloader(&DownloaderConfig{ downloader := NewDownloader(&DownloaderConfig{
DownloadDirWhiteList: []string{"./downloads"}, WhiteDownloadDirs: []string{"./downloads"},
}) })
if err := downloader.Setup(); err != nil { if err := downloader.Setup(); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -137,13 +137,15 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
downloader.Clear() downloader.Clear()
}() }()
reqs := make([]*base.Request, 0) reqs := make([]*base.CreateTaskBatchItem, 0)
fileNames := make([]string, 0) fileNames := make([]string, 0)
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
reqs = append(reqs, req) reqs = append(reqs, &base.CreateTaskBatchItem{
Req: req,
})
if i == 0 { if i == 0 {
fileNames = append(fileNames, test.DownloadName) fileNames = append(fileNames, test.DownloadName)
} else { } else {
@@ -160,11 +162,14 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
} }
}) })
_, err := downloader.CreateDirectBatch(reqs, &base.Options{ _, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
Path: test.Dir, Reqs: reqs,
Name: test.DownloadName, Opts: &base.Options{
Extra: http.OptsExtra{ Path: test.Dir,
Connections: 4, Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
}, },
}) })
if err != nil { if err != nil {
@@ -499,13 +504,15 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
downloader.Clear() downloader.Clear()
}() }()
reqs := make([]*base.Request, 0) reqs := make([]*base.CreateTaskBatchItem, 0)
fileNames := make([]string, 0) fileNames := make([]string, 0)
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
reqs = append(reqs, req) reqs = append(reqs, &base.CreateTaskBatchItem{
Req: req,
})
if i == 0 { if i == 0 {
fileNames = append(fileNames, test.DownloadName) fileNames = append(fileNames, test.DownloadName)
} else { } else {
@@ -522,11 +529,14 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
} }
}) })
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{ taskIds, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
Path: test.Dir, Reqs: reqs,
Name: test.DownloadName, Opts: &base.Options{
Extra: http.OptsExtra{ Path: test.Dir,
Connections: 4, Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
}, },
}) })
if err != nil { if err != nil {
+47
View File
@@ -205,6 +205,31 @@ function testTimeout(){
xhr.send(); xhr.send();
}); });
} }
async function testFingerprint(fingerprint,ua){
__gopeed_setFingerprint(fingerprint);
const resp = await fetch(host+'/ua');
const data = await resp.json();
if(!data.user_agent.includes(ua)){
throw new Error('fingerprint test failed, user agent: ' + data.user_agent);
}
}
async function testFingerprintDefault(){
await testFingerprint('none', 'Go')
}
async function testFingerprintChrome(){
await testFingerprint('chrome', 'Chrome')
}
async function testFingerprintFirefox(){
await testFingerprint('firefox', 'Firefox')
}
async function testFingerprintSafari(){
await testFingerprint('safari', 'Safari')
}
`) `)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -288,6 +313,19 @@ function testTimeout(){
if err == nil || err.Error() != "timeout" { if err == nil || err.Error() != "timeout" {
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err) t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
} }
_, err = callTestFun(engine, "testFingerprintChrome")
if err != nil {
t.Fatal("testFingerprintChrome test failed", err)
}
_, err = callTestFun(engine, "testFingerprintFirefox")
if err != nil {
t.Fatal("testFingerprintFirefox test failed", err)
}
_, err = callTestFun(engine, "testFingerprintSafari")
if err != nil {
t.Fatal("testFingerprintSafari test failed", err)
}
} }
func TestFetchWithProxy(t *testing.T) { func TestFetchWithProxy(t *testing.T) {
@@ -470,6 +508,15 @@ func startServer() net.Listener {
w.Write([]byte("ok")) w.Write([]byte("ok"))
} }
}) })
mux.HandleFunc("/ua", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data := map[string]any{
"user_agent": r.UserAgent(),
}
buf, _ := json.Marshal(data)
w.WriteHeader(http.StatusOK)
w.Write(buf)
})
server.Handler = mux server.Handler = mux
go server.Serve(listener) go server.Serve(listener)
return listener return listener
+77 -51
View File
@@ -15,6 +15,7 @@ import (
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util" "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/imroc/req/v3"
) )
const ( const (
@@ -129,7 +130,8 @@ type XMLHttpRequest struct {
requestHeaders http.Header requestHeaders http.Header
responseHeaders http.Header responseHeaders http.Header
aborted bool aborted bool
proxyHandler func(r *http.Request) (*url.URL, error) client *req.Client
fingerprint string
WithCredentials bool `json:"withCredentials"` WithCredentials bool `json:"withCredentials"`
Upload *XMLHttpRequestUpload `json:"upload"` Upload *XMLHttpRequestUpload `json:"upload"`
@@ -163,25 +165,39 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
} }
func (xhr *XMLHttpRequest) Send(data goja.Value) { func (xhr *XMLHttpRequest) Send(data goja.Value) {
var req *http.Request setFingerprint(xhr.client, xhr.fingerprint)
var err error
d := xhr.parseData(data) d := xhr.parseData(data)
var ( var (
contentType string contentType string
contentLength int64 contentLength int64
isStringBody bool isStringBody bool
) )
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
req, err = http.NewRequest(xhr.method, xhr.url, nil) // Create request using req library
} else { reqBuilder := xhr.client.R()
// Set headers first
if xhr.requestHeaders != nil {
for key, values := range xhr.requestHeaders {
if len(values) > 0 {
// Merge multiple values with comma separator (HTTP standard)
mergedValue := strings.Join(values, ", ")
reqBuilder.SetHeader(key, mergedValue)
}
}
}
// Handle request body
if d != nil && xhr.method != "GET" && xhr.method != "HEAD" {
switch v := d.(type) { switch v := d.(type) {
case string: case string:
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v)) reqBuilder.SetBody(v)
contentType = "text/plain;charset=UTF-8" contentType = "text/plain;charset=UTF-8"
contentLength = int64(len(v)) contentLength = int64(len(v))
isStringBody = true isStringBody = true
case *file.File: case *file.File:
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader) reqBuilder.SetBody(v.Reader)
contentType = "application/octet-stream" contentType = "application/octet-stream"
contentLength = v.Size contentLength = v.Size
case *formdata.FormData: case *formdata.FormData:
@@ -203,43 +219,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
defer mw.Close() defer mw.Close()
mw.Send() mw.Send()
}() }()
req, err = http.NewRequest(xhr.method, xhr.url, pr) reqBuilder.SetBody(pr)
contentType = mw.FormDataContentType() contentType = mw.FormDataContentType()
contentLength = mw.Size() contentLength = mw.Size()
} }
} }
if err != nil {
xhr.callOnerror()
return
}
req.Header = xhr.requestHeaders
// Only string body can specify Content-Type header by user // Only string body can specify Content-Type header by user
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") { if contentType != "" && (!isStringBody || xhr.requestHeaders.Get("Content-Type") == "") {
req.Header.Set("Content-Type", contentType) reqBuilder.SetHeader("Content-Type", contentType)
} }
if contentLength > 0 {
req.ContentLength = contentLength // Set timeout
if xhr.Timeout > 0 {
xhr.client.SetTimeout(time.Duration(xhr.Timeout) * time.Millisecond)
} }
transport := &http.Transport{
Proxy: xhr.proxyHandler, // Configure redirect behavior
} xhr.client.SetRedirectPolicy(func(req *http.Request, via []*http.Request) error {
client := &http.Client{ if xhr.Redirect == redirectManual {
Transport: transport, return http.ErrUseLastResponse
Timeout: time.Duration(xhr.Timeout) * time.Millisecond, }
CheckRedirect: func(req *http.Request, via []*http.Request) error { if xhr.Redirect == redirectError {
if xhr.Redirect == redirectManual { return errors.New("redirect failed")
return http.ErrUseLastResponse }
} if len(via) > 20 {
if xhr.Redirect == redirectError { return errors.New("too many redirects")
return errors.New("redirect failed") }
} return nil
if len(via) > 20 { })
return errors.New("too many redirects")
} // Execute request
return nil resp, err := reqBuilder.Send(xhr.method, xhr.url)
},
}
resp, err := client.Do(req)
if err != nil { if err != nil {
// handle timeout error // handle timeout error
var ne net.Error var ne net.Error
@@ -254,35 +265,37 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
xhr.callOnerror() xhr.callOnerror()
return return
} }
defer resp.Body.Close()
xhr.Upload.callOnprogress(contentLength, contentLength) xhr.Upload.callOnprogress(contentLength, contentLength)
if !xhr.aborted { if !xhr.aborted {
xhr.Upload.callOnload() xhr.Upload.callOnload()
} }
responseUrl := resp.Request.URL // Set response URL (final URL after redirects)
responseUrl.Fragment = "" if resp.Response != nil && resp.Response.Request != nil && resp.Response.Request.URL != nil {
xhr.ResponseUrl = responseUrl.String() responseUrl := resp.Response.Request.URL
responseUrl.Fragment = ""
xhr.ResponseUrl = responseUrl.String()
} else {
xhr.ResponseUrl = xhr.url
}
// Set response headers
xhr.responseHeaders = resp.Header xhr.responseHeaders = resp.Header
xhr.Status = resp.StatusCode xhr.Status = resp.StatusCode
xhr.StatusText = resp.Status xhr.StatusText = resp.Status
xhr.doReadystatechange(2) xhr.doReadystatechange(2)
buf, err := io.ReadAll(resp.Body)
if err != nil { bodyBytes := resp.Bytes()
xhr.callOnerror()
return
}
xhr.doReadystatechange(3) xhr.doReadystatechange(3)
xhr.Response = string(buf) xhr.Response = string(bodyBytes)
xhr.ResponseText = xhr.Response xhr.ResponseText = xhr.Response
xhr.doReadystatechange(4) xhr.doReadystatechange(4)
respBodyLen := int64(len(buf)) respBodyLen := int64(len(bodyBytes))
xhr.callOnprogress(respBodyLen, respBodyLen) xhr.callOnprogress(respBodyLen, respBodyLen)
if !xhr.aborted { if !xhr.aborted {
xhr.callOnload() xhr.callOnload()
} }
return
} }
func (xhr *XMLHttpRequest) Abort() { func (xhr *XMLHttpRequest) Abort() {
@@ -293,6 +306,9 @@ func (xhr *XMLHttpRequest) Abort() {
} }
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string { func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
if xhr.responseHeaders == nil {
return ""
}
return strings.Join(xhr.responseHeaders.Values(key), ", ") return strings.Join(xhr.responseHeaders.Values(key), ", ")
} }
@@ -359,8 +375,15 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
return instanceValue return instanceValue
}) })
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
// Create req client with proxy support
client := req.C()
if proxyHandler != nil {
client.SetProxy(proxyHandler)
}
instance := &XMLHttpRequest{ instance := &XMLHttpRequest{
proxyHandler: proxyHandler, client: client,
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
Upload: &XMLHttpRequestUpload{ Upload: &XMLHttpRequestUpload{
EventProp: &EventProp{ EventProp: &EventProp{
eventListeners: make(map[string]func(event *ProgressEvent)), eventListeners: make(map[string]func(event *ProgressEvent)),
@@ -374,6 +397,9 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
instanceValue.SetPrototype(call.This.Prototype()) instanceValue.SetPrototype(call.This.Prototype())
return instanceValue return instanceValue
}) })
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
runtime.Set(FingerprintMagicKey, fingerprint)
})
if err := runtime.Set("ProgressEvent", progressEvent); err != nil { if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
return err return err
} }
@@ -0,0 +1,24 @@
package xhr
import "github.com/imroc/req/v3"
type Fingerprint string
const (
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
fingerprintChrome = "chrome"
fingerprintFirefox = "firefox"
fingerprintSafari = "safari"
)
func setFingerprint(client *req.Client, fingerprint string) {
switch fingerprint {
case fingerprintChrome:
client.ImpersonateChrome()
case fingerprintFirefox:
client.ImpersonateFirefox()
case fingerprintSafari:
client.ImpersonateSafari()
}
}
+13
View File
@@ -22,3 +22,16 @@ func AssertError[T error](err error) (t T, r bool) {
} }
return return
} }
func SafeGet[T any](vm *goja.Runtime, name string) T {
v := vm.Get(name)
if v == nil {
var init T
return init
}
if e, ok := v.Export().(T); ok {
return e
}
var init T
return init
}
+4 -4
View File
@@ -137,10 +137,10 @@ type DownloaderConfig struct {
Controller *controller.Controller Controller *controller.Controller
FetchManagers []fetcher.FetcherManager FetchManagers []fetcher.FetcherManager
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms) RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
Storage Storage Storage Storage
StorageDir string StorageDir string
DownloadDirWhiteList []string `json:"downloadDirWhiteList"` WhiteDownloadDirs []string
ProductionMode bool ProductionMode bool
+8 -2
View File
@@ -14,6 +14,12 @@ type OptsExtra struct {
// Stats for download // Stats for download
type Stats struct { type Stats struct {
// http stats Connections []*StatsConnection `json:"connections"`
// health indicators of http }
type StatsConnection struct {
Downloaded int64 `json:"downloaded"`
Completed bool `json:"completed"`
Failed bool `json:"failed"`
RetryTimes int `json:"retryTimes"`
} }
+4 -2
View File
@@ -58,13 +58,13 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
} }
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) { func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
var req model.CreateTaskBatch var req base.CreateTaskBatch
if ReadJson(r, w, &req) { if ReadJson(r, w, &req) {
if len(req.Reqs) == 0 { if len(req.Reqs) == 0 {
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam)) WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
return return
} }
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt) taskIds, err := Downloader.CreateDirectBatch(&req)
if err != nil { if err != nil {
WriteJson(w, model.NewErrorResult(err.Error())) WriteJson(w, model.NewErrorResult(err.Error()))
return return
@@ -309,6 +309,8 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
r.RequestURI = "" r.RequestURI = ""
r.URL = targetUrl r.URL = targetUrl
r.Host = targetUrl.Host r.Host = targetUrl.Host
r.Header.Del("Authorization")
r.Header.Del("X-Target-Uri")
resp, err := http.DefaultClient.Do(r) resp, err := http.DefaultClient.Do(r)
if err != nil { if err != nil {
writeError(w, err.Error()) writeError(w, err.Error())
+4 -11
View File
@@ -1,7 +1,6 @@
package model package model
import ( import (
"encoding/base64"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"io/fs" "io/fs"
) )
@@ -25,9 +24,9 @@ type StartConfig struct {
ProductionMode bool ProductionMode bool
WebEnable bool WebEnable bool
WebFS fs.FS WebFS fs.FS
WebBasicAuth *WebBasicAuth WebAuth *WebAuth
} }
func (cfg *StartConfig) Init() *StartConfig { func (cfg *StartConfig) Init() *StartConfig {
@@ -49,13 +48,7 @@ func (cfg *StartConfig) Init() *StartConfig {
return cfg return cfg
} }
type WebBasicAuth struct { type WebAuth struct {
Username string Username string
Password string Password string
} }
// Authorization returns the value of the Authorization header to be used in HTTP requests.
func (cfg *WebBasicAuth) Authorization() string {
userId := cfg.Username + ":" + cfg.Password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
}
-5
View File
@@ -7,8 +7,3 @@ type CreateTask struct {
Req *base.Request `json:"req"` Req *base.Request `json:"req"`
Opt *base.Options `json:"opt"` Opt *base.Options `json:"opt"`
} }
type CreateTaskBatch struct {
Reqs []*base.Request `json:"reqs"`
Opt *base.Options `json:"opt"`
}
+129 -12
View File
@@ -2,6 +2,10 @@ package rest
import ( import (
"context" "context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
@@ -10,12 +14,14 @@ import (
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors" "github.com/pkg/errors"
"io"
"io/fs" "io/fs"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
) )
@@ -23,6 +29,7 @@ import (
var ( var (
srv *http.Server srv *http.Server
runningPort int runningPort int
aesKey []byte
Downloader *download.Downloader Downloader *download.Downloader
) )
@@ -58,7 +65,9 @@ func Stop() {
}() }()
if srv != nil { if srv != nil {
if err := srv.Shutdown(context.TODO()); err != nil { shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed") Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
} }
} }
@@ -76,8 +85,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
startCfg.Init() startCfg.Init()
downloadCfg := &download.DownloaderConfig{ downloadCfg := &download.DownloaderConfig{
ProductionMode: startCfg.ProductionMode, ProductionMode: startCfg.ProductionMode,
RefreshInterval: startCfg.RefreshInterval, RefreshInterval: startCfg.RefreshInterval,
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
} }
if startCfg.Storage == model.StorageBolt { if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir) downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
@@ -95,6 +105,13 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
util.SafeRemove(startCfg.Address) util.SafeRemove(startCfg.Address)
} }
if startCfg.WebEnable {
aesKey = make([]byte, 32)
if _, err := rand.Read(aesKey); err != nil {
return nil, nil, errors.Wrap(err, "generate aes key failed")
}
}
listener, err := net.Listen(startCfg.Network, startCfg.Address) listener, err := net.Listen(startCfg.Network, startCfg.Address)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -125,15 +142,40 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension) r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension) r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
r.Path("/api/v1/proxy").HandlerFunc(DoProxy) r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
enableApiToken := startCfg.ApiToken != ""
enableBasicAuth := startCfg.WebEnable && startCfg.WebAuth != nil
if startCfg.WebEnable { if startCfg.WebEnable {
if enableBasicAuth {
r.Methods(http.MethodPost).Path("/api/web/login").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var loginReq model.WebAuth
if ReadJson(r, w, &loginReq) {
if loginReq.Username == startCfg.WebAuth.Username && loginReq.Password == startCfg.WebAuth.Password {
// Generate a login token, Username:Password:Timestamp
timestamp := time.Now().Unix()
tokenData := fmt.Sprintf("%s:%s:%d", loginReq.Username, loginReq.Password, timestamp)
token, err := aesEncrypt(aesKey, []byte(tokenData))
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(token))
return
}
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
})
}
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem))) r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem))) r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS))))) r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
} }
enableApiToken := startCfg.ApiToken != ""
enableBasicAuth := startCfg.WebEnable && startCfg.WebBasicAuth != nil
if enableApiToken || enableBasicAuth { if enableApiToken || enableBasicAuth {
writeUnauthorized := func(w http.ResponseWriter, r *http.Request) {
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
}
r.Use(func(h http.Handler) http.Handler { r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if enableApiToken { if enableApiToken {
@@ -145,19 +187,49 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
return return
} }
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized)) writeUnauthorized(w, r)
return return
} }
} }
if startCfg.WebEnable && startCfg.WebBasicAuth != nil { if enableBasicAuth {
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() { if !strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api/web/login" {
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
return return
} }
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
token := r.Header.Get("Authorization")
if token == "" {
writeUnauthorized(w, r)
return
}
token = strings.TrimPrefix(token, "Bearer ")
tokenData, err := aesDecrypt(aesKey, token)
if err != nil {
writeUnauthorized(w, r)
return
}
parts := strings.SplitN(string(tokenData), ":", 3)
username := parts[0]
password := parts[1]
timestamp, _ := strconv.Atoi(parts[2])
if username != startCfg.WebAuth.Username || password != startCfg.WebAuth.Password {
writeUnauthorized(w, r)
return
}
// Check if the token is expired (7 days)
if time.Now().Unix()-int64(timestamp) > 7*24*3600 {
writeUnauthorized(w, r)
return
}
h.ServeHTTP(w, r)
return
} }
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized)) writeUnauthorized(w, r)
}) })
}) })
} }
@@ -177,9 +249,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
}) })
srv = &http.Server{Handler: handlers.CORS( srv = &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}), handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "X-Api-Token", "X-Target-Uri"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}), handlers.AllowedOrigins([]string{"*"}),
handlers.AllowCredentials(),
)(r)} )(r)}
return srv, listener, nil return srv, listener, nil
} }
@@ -315,3 +388,47 @@ func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(v) json.NewEncoder(w).Encode(v)
} }
func aesEncrypt(key, data []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
cipherText := gcm.Seal(nonce, nonce, data, nil)
return base64.StdEncoding.EncodeToString(cipherText), nil
}
func aesDecrypt(key []byte, encryptedData string) ([]byte, error) {
cipherText, err := base64.StdEncoding.DecodeString(encryptedData)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(cipherText) < gcm.NonceSize() {
return nil, errors.New("ciphertext too short")
}
nonce, cipherText := cipherText[:gcm.NonceSize()], cipherText[gcm.NonceSize():]
return gcm.Open(nil, nonce, cipherText, nil)
}
+98 -8
View File
@@ -16,6 +16,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -140,11 +141,13 @@ func TestCreateDirectTask(t *testing.T) {
func TestCreateDirectTaskBatch(t *testing.T) { func TestCreateDirectTaskBatch(t *testing.T) {
doTest(func() { doTest(func() {
reqs := make([]*base.Request, 0) reqs := make([]*base.CreateTaskBatchItem, 0)
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
reqs = append(reqs, createReq.Req) reqs = append(reqs, &base.CreateTaskBatchItem{
Req: createReq.Req,
})
} }
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{ taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
Reqs: reqs, Reqs: reqs,
}) })
if len(taskIds) != len(reqs) { if len(taskIds) != len(reqs) {
@@ -153,6 +156,45 @@ func TestCreateDirectTaskBatch(t *testing.T) {
}) })
} }
func TestCreateDirectTaskBatchWithOpt(t *testing.T) {
doTest(func() {
reqs := make([]*base.CreateTaskBatchItem, 0)
for i := 0; i < 5; i++ {
item := &base.CreateTaskBatchItem{
Req: createReq.Req,
}
if i == 0 {
item.Opts = &base.Options{
Name: "spe_opt.data",
}
}
reqs = append(reqs, item)
}
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
Reqs: reqs,
Opts: &base.Options{
Name: "default_opt.data",
},
})
if len(taskIds) != len(reqs) {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
}
for i, taskId := range taskIds {
task := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
if i == 0 {
if !strings.Contains(task.Name(), "spe_opt") {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "spe_opt.data")
}
} else {
if !strings.Contains(task.Name(), "default_opt") {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "default_opt.data")
}
}
}
})
}
func TestCreateDirectTaskWithResoled(t *testing.T) { func TestCreateDirectTaskWithResoled(t *testing.T) {
doTest(func() { doTest(func() {
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -595,7 +637,7 @@ func TestAuthorization(t *testing.T) {
cfg.Init() cfg.Init()
cfg.ApiToken = "123456" cfg.ApiToken = "123456"
cfg.WebEnable = true cfg.WebEnable = true
cfg.WebBasicAuth = &model.WebBasicAuth{ cfg.WebAuth = &model.WebAuth{
Username: "admin", Username: "admin",
Password: "123456", Password: "123456",
} }
@@ -607,14 +649,61 @@ func TestAuthorization(t *testing.T) {
Stop() Stop()
}() }()
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil) status, _ := doHttpRequest0(http.MethodPost, "/api/web/login", nil, &model.WebAuth{
Username: "xxx",
Password: "xxx",
})
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
token := httpRequestCheckOk[string](http.MethodPost, "/api/web/login", cfg.WebAuth)
authToken := fmt.Sprintf("Bearer %s", token)
authHeaders := map[string]string{
"Authorization": authToken,
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized { if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized) t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
} }
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{ status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(), "Authorization": "xxx",
}, nil) }, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": "xxx",
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
buildToken := func(username, password string, ts int64) string {
token, _ := aesEncrypt(aesKey, []byte(fmt.Sprintf("%s:%s:%d", username, password, ts)))
return token
}
fakeToken := buildToken("fake", "fake", time.Now().Unix())
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", fakeToken),
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
expireToken := buildToken(cfg.WebAuth.Username, cfg.WebAuth.Password, time.Now().Add(-time.Hour*8*24).Unix())
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", expireToken),
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", authHeaders, nil)
if status != http.StatusOK { if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK) t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
} }
@@ -627,7 +716,7 @@ func TestAuthorization(t *testing.T) {
} }
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{ status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(), "Authorization": authToken,
"X-Api-Token": cfg.ApiToken, "X-Api-Token": cfg.ApiToken,
}, nil) }, nil)
if status != http.StatusOK { if status != http.StatusOK {
@@ -635,7 +724,7 @@ func TestAuthorization(t *testing.T) {
} }
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{ status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(), "Authorization": authToken,
"X-Api-Token": "", "X-Api-Token": "",
}, nil) }, nil)
if status != http.StatusUnauthorized { if status != http.StatusUnauthorized {
@@ -666,6 +755,7 @@ func doTest0(onStart func(cfg *model.StartConfig), handler func()) {
Downloader.Clear() Downloader.Clear()
}() }()
defer func() { defer func() {
time.Sleep(500 * time.Millisecond)
Downloader.Pause(nil) Downloader.Pause(nil)
Downloader.Delete(nil, true) Downloader.Delete(nil, true)
os.RemoveAll(cfg.StorageDir) os.RemoveAll(cfg.StorageDir)
+10
View File
@@ -56,3 +56,13 @@ func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error)
} }
return http.ProxyURL(proxyUrl) return http.ProxyURL(proxyUrl)
} }
// TryUrlQueryUnescape tries to unescape a URL-encoded string.
//
// If unescaping fails, it returns the original string.
func TryUrlQueryUnescape(s string) string {
if decoded, err := url.QueryUnescape(s); err == nil {
return decoded
}
return s
}
+21
View File
@@ -128,3 +128,24 @@ func TestParseDataUri(t *testing.T) {
}) })
} }
} }
func TestTryURLDecode(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"normal.txt", "normal.txt"},
{"%E7%8F%80%E5%B0%94%E8%AF%BA.zip", "珀尔诺.zip"},
{"hello%20world.txt", "hello world.txt"},
{"bad%2-text", "bad%2-text"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := TryUrlQueryUnescape(tt.input)
if got != tt.expected {
t.Errorf("TryUrlQueryUnescape(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
+68 -32
View File
@@ -3,19 +3,24 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:gopeed/api/model/extension.dart'; import 'package:get/get.dart' as getx;
import 'package:gopeed/api/model/install_extension.dart';
import 'package:gopeed/api/model/switch_extension.dart';
import '../app/routes/app_pages.dart';
import '../database/database.dart';
import '../util/util.dart'; import '../util/util.dart';
import 'model/create_task.dart'; import 'model/create_task.dart';
import 'model/create_task_batch.dart';
import 'model/downloader_config.dart'; import 'model/downloader_config.dart';
import 'model/extension.dart';
import 'model/install_extension.dart';
import 'model/login.dart';
import 'model/request.dart'; import 'model/request.dart';
import 'model/resolve_result.dart'; import 'model/resolve_result.dart';
import 'model/result.dart'; import 'model/result.dart';
import 'model/switch_extension.dart';
import 'model/task.dart'; import 'model/task.dart';
import 'model/update_extension_settings.dart';
import 'model/update_check_extension_resp.dart'; import 'model/update_check_extension_resp.dart';
import 'model/update_extension_settings.dart';
class _Client { class _Client {
static _Client? _instance; static _Client? _instance;
@@ -29,12 +34,12 @@ class _Client {
_instance = _Client._internal(); _instance = _Client._internal();
var dio = Dio(); var dio = Dio();
final isUnixSocket = network == 'unix'; final isUnixSocket = network == 'unix';
var baseUrl = 'http://127.0.0.1'; var baseUrl = 'http://127.0.0.1/';
if (!isUnixSocket) { if (!isUnixSocket) {
if (Util.isWeb()) { if (Util.isWeb()) {
baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : ''; baseUrl = kDebugMode ? 'http://127.0.0.1:9999/' : '';
} else { } else {
baseUrl = 'http://$address'; baseUrl = 'http://$address/';
} }
} }
dio.options.baseUrl = baseUrl; dio.options.baseUrl = baseUrl;
@@ -42,12 +47,27 @@ class _Client {
dio.options.sendTimeout = const Duration(seconds: 5); dio.options.sendTimeout = const Duration(seconds: 5);
dio.options.connectTimeout = const Duration(seconds: 5); dio.options.connectTimeout = const Duration(seconds: 5);
dio.options.receiveTimeout = const Duration(seconds: 60); dio.options.receiveTimeout = const Duration(seconds: 60);
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) { dio.interceptors.add(InterceptorsWrapper(
if (apiToken.isNotEmpty) { onRequest: (options, handler) {
options.headers['X-Api-Token'] = apiToken; if (apiToken.isNotEmpty) {
} options.headers['X-Api-Token'] = apiToken;
handler.next(options); }
})); if (Util.isWeb()) {
final token = Database.instance.getWebToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
}
handler.next(options);
},
onError: (error, handler) {
// Only web version has a login page
if (Util.isWeb() && error.response?.statusCode == 401) {
getx.Get.rootDelegate.offAndToNamed(Routes.LOGIN);
}
handler.next(error);
},
));
_instance!.dio = dio; _instance!.dio = dio;
if (isUnixSocket) { if (isUnixSocket) {
@@ -95,7 +115,8 @@ Future<T> _parse<T>(
} on DioException catch (e) { } on DioException catch (e) {
if (e.type == DioExceptionType.sendTimeout || if (e.type == DioExceptionType.sendTimeout ||
e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.connectionTimeout) { e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.connectionError) {
throw TimeoutException("request timeout"); throw TimeoutException("request timeout");
} }
throw Exception(Result(code: 1000, msg: e.message)); throw Exception(Result(code: 1000, msg: e.message));
@@ -104,16 +125,22 @@ Future<T> _parse<T>(
Future<ResolveResult> resolve(Request request) async { Future<ResolveResult> resolve(Request request) async {
return _parse<ResolveResult>( return _parse<ResolveResult>(
() => _client.dio.post("/api/v1/resolve", data: request), () => _client.dio.post("api/v1/resolve", data: request),
(data) => ResolveResult.fromJson(data)); (data) => ResolveResult.fromJson(data));
} }
Future<String> createTask(CreateTask createTask) async { Future<String> createTask(CreateTask createTask) async {
return _parse<String>( return _parse<String>(
() => _client.dio.post("/api/v1/tasks", data: createTask), () => _client.dio.post("api/v1/tasks", data: createTask),
(data) => data as String); (data) => data as String);
} }
Future<List<String>> createTaskBatch(CreateTaskBatch createTaskBatch) async {
return _parse<List<String>>(
() => _client.dio.post("api/v1/tasks/batch", data: createTaskBatch),
(data) => (data as List).map((e) => e as String).toList());
}
Future<List<Task>> getTasks(List<Status> statuses) async { Future<List<Task>> getTasks(List<Status> statuses) async {
return _parse<List<Task>>( return _parse<List<Task>>(
() => _client.dio.get( () => _client.dio.get(
@@ -122,16 +149,16 @@ Future<List<Task>> getTasks(List<Status> statuses) async {
} }
Future<void> pauseTask(String id) async { Future<void> pauseTask(String id) async {
return _parse(() => _client.dio.put("/api/v1/tasks/$id/pause"), null); return _parse(() => _client.dio.put("api/v1/tasks/$id/pause"), null);
} }
Future<void> continueTask(String id) async { Future<void> continueTask(String id) async {
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null); return _parse(() => _client.dio.put("api/v1/tasks/$id/continue"), null);
} }
Future<void> pauseAllTasks(List<String>? ids) async { Future<void> pauseAllTasks(List<String>? ids) async {
return _parse( return _parse(
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: { () => _client.dio.put("api/v1/tasks/pause", queryParameters: {
"id": ids, "id": ids,
}), }),
null); null);
@@ -139,7 +166,7 @@ Future<void> pauseAllTasks(List<String>? ids) async {
Future<void> continueAllTasks(List<String>? ids) async { Future<void> continueAllTasks(List<String>? ids) async {
return _parse( return _parse(
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: { () => _client.dio.put("api/v1/tasks/continue", queryParameters: {
"id": ids, "id": ids,
}), }),
null); null);
@@ -147,12 +174,12 @@ Future<void> continueAllTasks(List<String>? ids) async {
Future<void> deleteTask(String id, bool force) async { Future<void> deleteTask(String id, bool force) async {
return _parse( return _parse(
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null); () => _client.dio.delete("api/v1/tasks/$id?force=$force"), null);
} }
Future<void> deleteTasks(List<String>? ids, bool force) async { Future<void> deleteTasks(List<String>? ids, bool force) async {
return _parse( return _parse(
() => _client.dio.delete("/api/v1/tasks", queryParameters: { () => _client.dio.delete("api/v1/tasks", queryParameters: {
"id": ids, "id": ids,
"force": force, "force": force,
}), }),
@@ -160,29 +187,29 @@ Future<void> deleteTasks(List<String>? ids, bool force) async {
} }
Future<DownloaderConfig> getConfig() async { Future<DownloaderConfig> getConfig() async {
return _parse(() => _client.dio.get("/api/v1/config"), return _parse(() => _client.dio.get("api/v1/config"),
(data) => DownloaderConfig.fromJson(data)); (data) => DownloaderConfig.fromJson(data));
} }
Future<void> putConfig(DownloaderConfig config) async { Future<void> putConfig(DownloaderConfig config) async {
return _parse(() => _client.dio.put("/api/v1/config", data: config), null); return _parse(() => _client.dio.put("api/v1/config", data: config), null);
} }
Future<void> installExtension(InstallExtension installExtension) async { Future<void> installExtension(InstallExtension installExtension) async {
return _parse( return _parse(
() => _client.dio.post("/api/v1/extensions", data: installExtension), () => _client.dio.post("api/v1/extensions", data: installExtension),
null); null);
} }
Future<List<Extension>> getExtensions() async { Future<List<Extension>> getExtensions() async {
return _parse<List<Extension>>(() => _client.dio.get("/api/v1/extensions"), return _parse<List<Extension>>(() => _client.dio.get("api/v1/extensions"),
(data) => (data as List).map((e) => Extension.fromJson(e)).toList()); (data) => (data as List).map((e) => Extension.fromJson(e)).toList());
} }
Future<void> updateExtensionSettings( Future<void> updateExtensionSettings(
String identity, UpdateExtensionSettings updateExtensionSettings) async { String identity, UpdateExtensionSettings updateExtensionSettings) async {
return _parse( return _parse(
() => _client.dio.put("/api/v1/extensions/$identity/settings", () => _client.dio.put("api/v1/extensions/$identity/settings",
data: updateExtensionSettings), data: updateExtensionSettings),
null); null);
} }
@@ -191,22 +218,27 @@ Future<void> switchExtension(
String identity, SwitchExtension switchExtension) async { String identity, SwitchExtension switchExtension) async {
return _parse( return _parse(
() => _client.dio () => _client.dio
.put("/api/v1/extensions/$identity/switch", data: switchExtension), .put("api/v1/extensions/$identity/switch", data: switchExtension),
null); null);
} }
Future<void> deleteExtension(String identity) async { Future<void> deleteExtension(String identity) async {
return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null); return _parse(() => _client.dio.delete("api/v1/extensions/$identity"), null);
} }
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async { Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"), return _parse(() => _client.dio.get("api/v1/extensions/$identity/update"),
(data) => UpdateCheckExtensionResp.fromJson(data)); (data) => UpdateCheckExtensionResp.fromJson(data));
} }
Future<void> updateExtension(String identity) async { Future<void> updateExtension(String identity) async {
return _parse( return _parse(
() => _client.dio.post("/api/v1/extensions/$identity/update"), null); () => _client.dio.post("api/v1/extensions/$identity/update"), null);
}
Future<String> login(LoginReq loginReq) async {
return _parse(() => _client.dio.post("api/web/login", data: loginReq),
(data) => data as String);
} }
Future<Response<String>> proxyRequest<T>(String uri, Future<Response<String>> proxyRequest<T>(String uri,
@@ -223,5 +255,9 @@ Future<Response<String>> proxyRequest<T>(String uri,
} }
String join(String path) { String join(String path) {
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}"; final baseUrl = _client.dio.options.baseUrl;
final cleanBaseUrl = baseUrl.endsWith('/')
? baseUrl.substring(0, baseUrl.length - 1)
: baseUrl;
return "$cleanBaseUrl/${Util.cleanPath(path)}";
} }
@@ -0,0 +1,39 @@
import 'package:json_annotation/json_annotation.dart';
import 'options.dart';
import 'request.dart';
part 'create_task_batch.g.dart';
@JsonSerializable(explicitToJson: true)
class CreateTaskBatch {
List<CreateTaskBatchItem>? reqs;
Options? opt;
CreateTaskBatch({
this.reqs,
this.opt,
});
factory CreateTaskBatch.fromJson(
Map<String, dynamic> json,
) =>
_$CreateTaskBatchFromJson(json);
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
}
@JsonSerializable()
class CreateTaskBatchItem {
Request? req;
Options? opts;
CreateTaskBatchItem({
this.req,
this.opts,
});
factory CreateTaskBatchItem.fromJson(Map<String, dynamic> json) =>
_$CreateTaskBatchItemFromJson(json);
Map<String, dynamic> toJson() => _$CreateTaskBatchItemToJson(this);
}
@@ -0,0 +1,55 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'create_task_batch.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CreateTaskBatch _$CreateTaskBatchFromJson(Map<String, dynamic> json) =>
CreateTaskBatch(
reqs: (json['reqs'] as List<dynamic>?)
?.map((e) => CreateTaskBatchItem.fromJson(e as Map<String, dynamic>))
.toList(),
opt: json['opt'] == null
? null
: Options.fromJson(json['opt'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('reqs', instance.reqs?.map((e) => e.toJson()).toList());
writeNotNull('opt', instance.opt?.toJson());
return val;
}
CreateTaskBatchItem _$CreateTaskBatchItemFromJson(Map<String, dynamic> json) =>
CreateTaskBatchItem(
req: json['req'] == null
? null
: Request.fromJson(json['req'] as Map<String, dynamic>),
opts: json['opts'] == null
? null
: Options.fromJson(json['opts'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateTaskBatchItemToJson(CreateTaskBatchItem instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('req', instance.req);
writeNotNull('opts', instance.opts);
return val;
}
@@ -81,6 +81,7 @@ class ExtraConfig {
bool lastDeleteTaskKeep; bool lastDeleteTaskKeep;
bool defaultDirectDownload; bool defaultDirectDownload;
bool defaultBtClient; bool defaultBtClient;
bool notifyWhenNewVersion;
ExtraConfigBt bt = ExtraConfigBt(); ExtraConfigBt bt = ExtraConfigBt();
@@ -90,6 +91,7 @@ class ExtraConfig {
this.lastDeleteTaskKeep = false, this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false, this.defaultDirectDownload = false,
this.defaultBtClient = true, this.defaultBtClient = true,
this.notifyWhenNewVersion = true,
}); });
factory ExtraConfig.fromJson(Map<String, dynamic>? json) => factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
@@ -74,6 +74,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false, lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false, defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
defaultBtClient: json['defaultBtClient'] as bool? ?? true, defaultBtClient: json['defaultBtClient'] as bool? ?? true,
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>); )..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) => Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
@@ -83,6 +84,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep, 'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
'defaultDirectDownload': instance.defaultDirectDownload, 'defaultDirectDownload': instance.defaultDirectDownload,
'defaultBtClient': instance.defaultBtClient, 'defaultBtClient': instance.defaultBtClient,
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
'bt': instance.bt.toJson(), 'bt': instance.bt.toJson(),
}; };
+18
View File
@@ -0,0 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
part 'login.g.dart';
@JsonSerializable()
class LoginReq {
String username;
String password;
LoginReq({
required this.username,
required this.password,
});
factory LoginReq.fromJson(Map<String, dynamic> json) =>
_$LoginReqFromJson(json);
Map<String, dynamic> toJson() => _$LoginReqToJson(this);
}
Loaded 50 of 80 files, more files were not shown because too many files have changed in this diff. Show more