mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 18:40:27 +00:00
Compare commits
95
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b329fee107 | ||
|
|
3ce95f21fd | ||
|
|
f54ed5df5a | ||
|
|
727b7edb84 | ||
|
|
f5891ee03d | ||
|
|
724276ac69 | ||
|
|
a3f559c8e5 | ||
|
|
edd189a8a8 | ||
|
|
c970378fd7 | ||
|
|
8b61343aef | ||
|
|
2cb93a03e2 | ||
|
|
81dfc073a6 | ||
|
|
427921ccab | ||
|
|
f4876c8a0e | ||
|
|
35a3625b11 | ||
|
|
dc9b501e35 | ||
|
|
85fe92a378 | ||
|
|
983d9ad09e | ||
|
|
d9f757aaf0 | ||
|
|
cd409fcfb9 | ||
|
|
366244fa5d | ||
|
|
7a2e9b4fd1 | ||
|
|
203385d3f8 | ||
|
|
aaf73dec24 | ||
|
|
2523282a89 | ||
|
|
3384f5197a | ||
|
|
cd77665f02 | ||
|
|
a22b431150 | ||
|
|
eb7c9ad7f2 | ||
|
|
ea11fc38f1 | ||
|
|
1be16e9f2c | ||
|
|
1d86d14a20 | ||
|
|
5ad15cc4ac | ||
|
|
49604966fd | ||
|
|
bdb8ed177f | ||
|
|
2dc7048cba | ||
|
|
d3750464aa | ||
|
|
5c9bcd3731 | ||
|
|
bdda550b2c | ||
|
|
3308d966f0 | ||
|
|
18041ed9ec | ||
|
|
56b85e545e | ||
|
|
c5af916952 | ||
|
|
a1981328aa | ||
|
|
51244133e0 | ||
|
|
0c17093b1a | ||
|
|
6538b29b62 | ||
|
|
81e90e6b89 | ||
|
|
4f39296664 | ||
|
|
ece5b91dc2 | ||
|
|
d9f04ae928 | ||
|
|
e3ccecbd9c | ||
|
|
fb1c2cb060 | ||
|
|
9bff75464d | ||
|
|
863f759248 | ||
|
|
f89fb5e438 | ||
|
|
7ca1cc8c64 | ||
|
|
1f791c66e6 | ||
|
|
4d60c47d39 | ||
|
|
3fd3dd49d2 | ||
|
|
d119de7b98 | ||
|
|
69b359a9c9 | ||
|
|
4fbe13a1f0 | ||
|
|
3b50465708 | ||
|
|
473c58796c | ||
|
|
fab8f135f3 | ||
|
|
0fca7b6bc7 | ||
|
|
4700d0ba28 | ||
|
|
6e31ea089f | ||
|
|
4a2088f6fb | ||
|
|
17c8fb7e4d | ||
|
|
8c99bbc847 | ||
|
|
06c7fe7a22 | ||
|
|
8b3b5d4394 | ||
|
|
2ee73ab619 | ||
|
|
15413dbc95 | ||
|
|
c7c43fac80 | ||
|
|
4283bdfa5a | ||
|
|
328ba8592c | ||
|
|
a191a4140d | ||
|
|
3521837964 | ||
|
|
b30cd78f86 | ||
|
|
0583e4aaa6 | ||
|
|
a89c55b5c4 | ||
|
|
68c40d7f3f | ||
|
|
42a0b89d95 | ||
|
|
1860f534cd | ||
|
|
5c2d2ad7b4 | ||
|
|
db6352ada8 | ||
|
|
d1a5ed8b3d | ||
|
|
be9583be0f | ||
|
|
d2c70b077b | ||
|
|
53b3eea4fc | ||
|
|
54347f41b6 | ||
|
|
cea454b015 |
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
.git
|
||||||
|
|
||||||
|
*.data
|
||||||
|
*.log
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
.github
|
||||||
|
_docs
|
||||||
|
_examples
|
||||||
|
bin
|
||||||
|
ui
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* @monkeyWie
|
||||||
@@ -11,6 +11,9 @@ categories:
|
|||||||
- 'fix'
|
- 'fix'
|
||||||
- 'bugfix'
|
- 'bugfix'
|
||||||
- 'bug'
|
- 'bug'
|
||||||
|
- title: '🔧 Performance Improvements'
|
||||||
|
labels:
|
||||||
|
- 'perf'
|
||||||
- title: '🧪 Tests'
|
- title: '🧪 Tests'
|
||||||
label: 'test'
|
label: 'test'
|
||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
|
|||||||
+91
-17
@@ -13,7 +13,7 @@ on:
|
|||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.21"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
|
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
|
||||||
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
|
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
|
||||||
|
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
$TAG = "v$env:VERSION"
|
$TAG = "v$env:VERSION"
|
||||||
flutter build windows
|
flutter build windows
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
build-macos-arm64-lib:
|
build-macos-arm64-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: ubuntu-latest
|
runs-on: macos-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -147,16 +147,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go install src.techknowlogick.com/xgo@latest
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
|
||||||
mv libgopeed-*.dylib libgopeed.dylib
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-arm64-lib
|
||||||
path: libgopeed.dylib
|
path: libgopeed.dylib
|
||||||
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-latest
|
runs-on: macos-13
|
||||||
needs: [ get-release, build-macos-arm64-lib ]
|
needs: [ get-release, build-macos-arm64-lib ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -171,7 +169,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Install appdmg
|
- name: Install appdmg
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install setuptools
|
python3 -m pip install setuptools --break-system-packages
|
||||||
npm install -g appdmg
|
npm install -g appdmg
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -260,9 +258,6 @@ jobs:
|
|||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
- name: Setup LXD
|
- name: Setup LXD
|
||||||
uses: canonical/[email protected]
|
uses: canonical/[email protected]
|
||||||
with:
|
with:
|
||||||
@@ -300,20 +295,55 @@ jobs:
|
|||||||
|
|
||||||
apps:
|
apps:
|
||||||
gopeed:
|
gopeed:
|
||||||
command: gopeed
|
command: bin/gopeed
|
||||||
extensions: [gnome]
|
extensions: [gnome]
|
||||||
plugs:
|
plugs:
|
||||||
- network
|
- network
|
||||||
|
- home
|
||||||
slots:
|
slots:
|
||||||
- dbus-gopeed
|
- dbus-gopeed
|
||||||
parts:
|
parts:
|
||||||
|
flutter-git:
|
||||||
|
source: https://github.com/flutter/flutter.git
|
||||||
|
source-tag: $FLUTTER_VERSION
|
||||||
|
source-depth: 1
|
||||||
|
plugin: nil
|
||||||
|
override-build: |
|
||||||
|
mkdir -p \$CRAFT_PART_INSTALL/usr/bin
|
||||||
|
mkdir -p \$CRAFT_PART_INSTALL/usr/libexec
|
||||||
|
cp -r \$CRAFT_PART_SRC \$CRAFT_PART_INSTALL/usr/libexec/flutter
|
||||||
|
ln -s \$CRAFT_PART_INSTALL/usr/libexec/flutter/bin/flutter \$CRAFT_PART_INSTALL/usr/bin/flutter
|
||||||
|
ln -s \$SNAPCRAFT_PART_INSTALL/usr/libexec/flutter/bin/dart \$SNAPCRAFT_PART_INSTALL/usr/bin/dart
|
||||||
|
\$CRAFT_PART_INSTALL/usr/bin/flutter doctor
|
||||||
|
build-packages:
|
||||||
|
- clang
|
||||||
|
- cmake
|
||||||
|
- curl
|
||||||
|
- libgtk-3-dev
|
||||||
|
- ninja-build
|
||||||
|
- unzip
|
||||||
|
- xz-utils
|
||||||
|
- zip
|
||||||
|
override-prime: ''
|
||||||
|
zenity:
|
||||||
|
plugin: nil
|
||||||
|
stage-packages:
|
||||||
|
- zenity
|
||||||
gopeed:
|
gopeed:
|
||||||
|
after: [flutter-git,zenity]
|
||||||
source: .
|
source: .
|
||||||
plugin: flutter
|
plugin: nil
|
||||||
stage-packages:
|
stage-packages:
|
||||||
- libgtk-3-dev
|
- libgtk-3-dev
|
||||||
- libappindicator3-dev
|
- libappindicator3-dev
|
||||||
flutter-target: lib/main.dart
|
override-build: |
|
||||||
|
# work around pub get stack overflow # https://github.com/dart-lang/sdk/issues/51068#issuecomment-1396588253
|
||||||
|
set +e
|
||||||
|
dart pub get
|
||||||
|
set -eux
|
||||||
|
flutter build linux --release -v
|
||||||
|
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
||||||
|
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
||||||
@@ -350,7 +380,7 @@ jobs:
|
|||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
go get golang.org/x/mobile/bind
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
gomobile init
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
flutter build apk
|
flutter build apk
|
||||||
@@ -381,7 +411,7 @@ jobs:
|
|||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
go get golang.org/x/mobile/bind
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
gomobile init
|
||||||
gomobile bind -tags nosqlite,netgo -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build ios --no-codesign
|
flutter build ios --no-codesign
|
||||||
mkdir Payload
|
mkdir Payload
|
||||||
@@ -411,6 +441,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
|
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --web-renderer html
|
flutter build web --web-renderer html
|
||||||
cd ../../
|
cd ../../
|
||||||
@@ -438,6 +469,39 @@ jobs:
|
|||||||
|| true
|
|| true
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
sudo apt update -y
|
||||||
|
sudo apt install -y pv python2 bsdmainutils
|
||||||
|
wget -O qdk2_0.32.bionic_amd64.deb "https://github.com/qnap-dev/qdk2/releases/download/v0.32/qdk2_0.32.bionic_amd64.deb"
|
||||||
|
dpkg -X qdk2_0.32.bionic_amd64.deb qdk2 # Direct installs will fail due to missing dependencies!
|
||||||
|
[[ -d qdk2 ]] || exit 1
|
||||||
|
export PATH=$(pwd)/qdk2/usr/bin:$(pwd)/qdk2/usr/share/qdk2/QDK/bin:${PATH}
|
||||||
|
wget -O Gopeed.template.tar.gz "https://github.com/GopeedLab/QpkgBuild/raw/refs/heads/master/template/Gopeed.template.tar.gz"
|
||||||
|
tar -zxf Gopeed.template.tar.gz
|
||||||
|
[[ -d Gopeed ]] || exit 1
|
||||||
|
goos=linux
|
||||||
|
goarch_arr=(amd64 arm64)
|
||||||
|
for goarch in "${goarch_arr[@]}"; do
|
||||||
|
qarch=x86_64
|
||||||
|
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
|
||||||
|
name=gopeed-web-v${VERSION}-${goos}-${goarch}
|
||||||
|
cp dist/${name}/${name}/* Gopeed/${qarch}/
|
||||||
|
done
|
||||||
|
cd Gopeed
|
||||||
|
sed -i -e 's/__QPKG_VER__/${VERSION}/g' qpkg.cfg
|
||||||
|
qbuild || exit 1
|
||||||
|
for f in build/*; do echo ">>> build=[${f}]"; done
|
||||||
|
goos=qnap
|
||||||
|
goarch_arr=(amd64 arm64)
|
||||||
|
for goarch in "${goarch_arr[@]}"; do
|
||||||
|
qarch=x86_64
|
||||||
|
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
|
||||||
|
sname=Gopeed_${VERSION}_${qarch}.qpkg
|
||||||
|
dname=gopeed-${goos}-v${VERSION}-${goarch}.qpkg
|
||||||
|
echo "sname=${sname}, dname=${dname}"
|
||||||
|
[[ -f build/${sname} ]] && cp -ra build/${sname} ../dist/zip/${dname}
|
||||||
|
done
|
||||||
|
for f in ../dist/zip/*; do echo ">>> dist=[${f}]"; done
|
||||||
|
cd ..
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -453,6 +517,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo rm -rf /usr/share/dotnet
|
sudo rm -rf /usr/share/dotnet
|
||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
@@ -463,11 +530,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: liwei2633
|
username: liwei2633
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build flutter web
|
||||||
|
run: |
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web --web-renderer html
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
with:
|
with:
|
||||||
|
context: .
|
||||||
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ env.VERSION }}
|
VERSION=${{ env.VERSION }}
|
||||||
@@ -477,7 +552,6 @@ jobs:
|
|||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm64
|
linux/arm64
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
linux/arm64/v8
|
|
||||||
tags: |
|
tags: |
|
||||||
liwei2633/gopeed:latest
|
liwei2633/gopeed:latest
|
||||||
liwei2633/gopeed:v${{ env.VERSION }}
|
liwei2633/gopeed:v${{ env.VERSION }}
|
||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.23"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
+98
-11
@@ -27,7 +27,7 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.21"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -54,14 +54,49 @@ jobs:
|
|||||||
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: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
build:
|
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||||
name: Build check
|
build-desktop:
|
||||||
runs-on: ubuntu-latest
|
name: Build desktop check
|
||||||
# needs: [lint, test]
|
runs-on: ${{ matrix.os }}
|
||||||
needs: [test]
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ windows-latest, macos-latest, ubuntu-latest ]
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- if: runner.os == 'windows'
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build windows
|
||||||
|
- if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build macos
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build linux
|
||||||
|
build-mobile:
|
||||||
|
name: Build mobile check
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos-latest, ubuntu-latest ]
|
||||||
|
needs: [ test ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -71,10 +106,62 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
- if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ipa --no-codesign
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: "zulu"
|
||||||
|
java-version: "17"
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build apk
|
||||||
|
|
||||||
|
build-web:
|
||||||
|
name: Build web check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build linux
|
flutter build web --web-renderer html
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
go build -tags nosqlite,web -ldflags="-s -w" github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
build-docker:
|
||||||
|
name: Build docker check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web --web-renderer html
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
docker build -t gopeed .
|
||||||
@@ -21,6 +21,7 @@ The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n
|
|||||||
You only need to add the corresponding language file in this directory.
|
You only need to add the corresponding language file in this directory.
|
||||||
|
|
||||||
Please refer to `en_us.dart` for translation.
|
Please refer to `en_us.dart` for translation.
|
||||||
|
words prefixed with `@` are not meant to be translated.
|
||||||
|
|
||||||
## flutter development
|
## flutter development
|
||||||
|
|
||||||
|
|||||||
+3
-12
@@ -1,29 +1,20 @@
|
|||||||
FROM instrumentisto/flutter:3.16.8 AS flutter
|
FROM golang:1.23.3 AS go
|
||||||
WORKDIR /app
|
|
||||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
|
||||||
RUN flutter pub get
|
|
||||||
COPY ./ui/flutter ./
|
|
||||||
RUN flutter build web --web-renderer html
|
|
||||||
|
|
||||||
FROM golang:1.21.6 AS go
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./go.mod ./go.sum ./
|
COPY ./go.mod ./go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=flutter /app/build/web ./cmd/web/dist
|
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
RUN CGO_ENABLED=0 go build -tags nosqlite,web \
|
RUN CGO_ENABLED=0 go build -tags nosqlite,web \
|
||||||
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
||||||
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
|
||||||
FROM alpine:3.14.2
|
FROM alpine:3.18
|
||||||
LABEL maintainer="monkeyWie"
|
LABEL maintainer="monkeyWie"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=go /app/dist/gopeed ./
|
COPY --from=go /app/dist/gopeed ./
|
||||||
COPY entrypoint.sh ./entrypoint.sh
|
COPY entrypoint.sh ./entrypoint.sh
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk upgrade --no-cache && \
|
apk add --no-cache su-exec ; \
|
||||||
apk add --no-cache bash su-exec; \
|
|
||||||
chmod +x ./entrypoint.sh && \
|
chmod +x ./entrypoint.sh && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
VOLUME ["/app/storage"]
|
VOLUME ["/app/storage"]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,7 +7,7 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.21+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
@@ -172,7 +174,7 @@ command:
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+5
-3
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,7 +7,7 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
|
|
||||||
### 環境
|
### 環境
|
||||||
|
|
||||||
1. Go 言語 1.21+
|
1. Go 言語 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### クローン
|
### クローン
|
||||||
@@ -160,7 +162,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+6
-4
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,7 +7,7 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ Dự án này được chia thành hai phần, phần giao diện sử dụng `f
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.21+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
@@ -168,7 +170,7 @@ command:
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
@@ -208,4 +210,4 @@ go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed
|
|||||||
|
|
||||||
## Giấy phép
|
## Giấy phép
|
||||||
|
|
||||||
[GPLv3](LICENSE)
|
[GPLv3](LICENSE)
|
||||||
+5
-3
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,7 +7,7 @@
|
|||||||
[](https://docs.gopeed.com/zh/donate.html)
|
[](https://docs.gopeed.com/zh/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
1. Golang 1.21+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆项目
|
### 克隆项目
|
||||||
@@ -172,7 +174,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+5
-3
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,7 +7,7 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|||||||
|
|
||||||
### 開發環境
|
### 開發環境
|
||||||
|
|
||||||
1. Golang 1.21+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆項目
|
### 克隆項目
|
||||||
@@ -168,7 +170,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 51 KiB |
+36
-11
@@ -8,7 +8,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed banner.txt
|
//go:embed banner.txt
|
||||||
@@ -25,23 +27,46 @@ func Start(cfg *model.StartConfig) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if downloadCfg.FirstLoad {
|
if downloadCfg.FirstLoad {
|
||||||
|
// Set default download config
|
||||||
|
if cfg.DownloadConfig != nil {
|
||||||
|
cfg.DownloadConfig.Merge(downloadCfg)
|
||||||
|
// TODO Use PatchConfig
|
||||||
|
rest.Downloader.PutConfig(cfg.DownloadConfig)
|
||||||
|
downloadCfg = cfg.DownloadConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadDir := downloadCfg.DownloadDir
|
||||||
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
|
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
|
||||||
var downloadDir string
|
if downloadDir == "" {
|
||||||
if base.InDocker == "true" {
|
if base.InDocker == "true" {
|
||||||
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||||
} else {
|
} else {
|
||||||
userDir, err := os.UserHomeDir()
|
userDir, err := os.UserHomeDir()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
downloadDir = filepath.Join(userDir, "Downloads")
|
downloadDir = filepath.Join(userDir, "Downloads")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if downloadDir != "" {
|
||||||
|
downloadCfg.DownloadDir = downloadDir
|
||||||
|
rest.Downloader.PutConfig(downloadCfg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if downloadDir != "" {
|
|
||||||
downloadCfg.DownloadDir = downloadDir
|
|
||||||
rest.Downloader.PutConfig(downloadCfg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
watchExit()
|
||||||
|
|
||||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func watchExit() {
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
sig := <-sigs
|
||||||
|
fmt.Printf("Server is shutting down due to signal: %s\n", sig)
|
||||||
|
rest.Downloader.Close()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
}
|
||||||
+6
-2
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,8 @@ type args struct {
|
|||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
ApiToken *string `json:"apiToken"`
|
ApiToken *string `json:"apiToken"`
|
||||||
StorageDir *string `json:"storageDir"`
|
StorageDir *string `json:"storageDir"`
|
||||||
|
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||||
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
configPath *string
|
configPath *string
|
||||||
}
|
}
|
||||||
@@ -25,7 +28,7 @@ func parse() *args {
|
|||||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
||||||
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
|
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
|
||||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -57,8 +60,9 @@ func loadConfig(path string) *args {
|
|||||||
var args args
|
var args args
|
||||||
|
|
||||||
if !filepath.IsAbs(path) {
|
if !filepath.IsAbs(path) {
|
||||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println("config dir get failed, reason:" + err.Error())
|
||||||
return &args
|
return &args
|
||||||
}
|
}
|
||||||
path = filepath.Join(dir, path)
|
path = filepath.Join(dir, path)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func main() {
|
|||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
StorageDir: filepath.Join(dir, "storage"),
|
StorageDir: filepath.Join(dir, "storage"),
|
||||||
ApiToken: *args.ApiToken,
|
ApiToken: *args.ApiToken,
|
||||||
|
DownloadConfig: args.DownloadConfig,
|
||||||
ProductionMode: true,
|
ProductionMode: true,
|
||||||
WebEnable: true,
|
WebEnable: true,
|
||||||
WebFS: sub,
|
WebFS: sub,
|
||||||
|
|||||||
+14
-11
@@ -1,12 +1,15 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gopeed:
|
gopeed:
|
||||||
container_name: gopeed
|
container_name: gopeed
|
||||||
ports:
|
ports:
|
||||||
- "9999:9999" # HTTP port (host:container)
|
- 9999:9999 # HTTP port (host:container)
|
||||||
image: liwei2633/gopeed
|
environment:
|
||||||
volumes:
|
- PUID=0
|
||||||
- ~/gopeed/Downloads:/root/Downloads # mount download path
|
- PGID=0
|
||||||
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
- UMASK=022
|
||||||
restart: unless-stopped
|
volumes:
|
||||||
|
- ~/gopeed/Downloads:/app/Downloads # mount download path
|
||||||
|
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
||||||
|
restart: unless-stopped
|
||||||
|
image: liwei2633/gopeed
|
||||||
|
# command: -u Username -p Password # optional authentication
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
chown -R ${PUID}:${PGID} /app
|
chown -R ${PUID}:${PGID} /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,117 +1,127 @@
|
|||||||
module github.com/GopeedLab/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.21
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
github.com/anacrolix/missinggo/v2 v2.8.0
|
||||||
github.com/anacrolix/torrent v1.53.3
|
github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||||
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/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/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.3.8
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/sync v0.6.0
|
golang.org/x/sync v0.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.7.0 // indirect
|
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/chansync v0.4.0 // indirect
|
github.com/anacrolix/chansync v0.6.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
github.com/anacrolix/dht/v2 v2.22.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.3.0 // indirect
|
github.com/anacrolix/envpprof v1.4.0 // indirect
|
||||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
|
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
||||||
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||||
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 // indirect
|
github.com/anacrolix/log v0.16.0 // indirect
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.1.1 // indirect
|
||||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
github.com/anacrolix/multiless v0.4.0 // indirect
|
||||||
github.com/anacrolix/stm v0.5.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.5.1 // indirect
|
github.com/anacrolix/sync v0.5.3 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.3 // indirect
|
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||||
github.com/anacrolix/utp v0.2.0 // indirect
|
github.com/anacrolix/utp v0.2.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.13.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/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // 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
|
||||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||||
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||||
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
|
github.com/go-llsqlite/crawshaw v0.5.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+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.2 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.1 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.4.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.5 // indirect
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/pion/ice/v2 v2.3.12 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pion/interceptor v0.1.25 // indirect
|
github.com/pion/datachannel v1.5.9 // indirect
|
||||||
|
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||||
|
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||||
|
github.com/pion/interceptor v0.1.37 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.9 // indirect
|
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.13 // indirect
|
github.com/pion/rtcp v1.2.14 // indirect
|
||||||
github.com/pion/rtp v1.8.3 // indirect
|
github.com/pion/rtp v1.8.9 // indirect
|
||||||
github.com/pion/sctp v1.8.9 // indirect
|
github.com/pion/sctp v1.8.33 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||||
github.com/pion/stun v0.6.1 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
github.com/pion/webrtc/v4 v4.0.2 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
github.com/stretchr/testify v1.8.4 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/tidwall/btree v1.7.0 // indirect
|
github.com/tidwall/btree v1.7.0 // indirect
|
||||||
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
golang.org/x/crypto v0.29.0 // indirect
|
||||||
golang.org/x/crypto v0.18.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/mod v0.14.0 // indirect
|
golang.org/x/net v0.31.0 // indirect
|
||||||
golang.org/x/net v0.20.0 // indirect
|
golang.org/x/sys v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/tools v0.27.0 // indirect
|
||||||
golang.org/x/tools v0.17.0 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
modernc.org/libc v1.40.6 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.7.2 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.28.0 // indirect
|
modernc.org/sqlite v1.33.1 // indirect
|
||||||
zombiezen.com/go/sqlite v1.1.0 // indirect
|
zombiezen.com/go/sqlite v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -5,17 +5,22 @@ crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||||
|
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||||
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||||
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
|
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||||
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||||
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
||||||
@@ -23,33 +28,36 @@ github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ
|
|||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
|
||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
|
||||||
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
|
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
|
||||||
|
github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
|
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
|
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
|
||||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
|
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
|
||||||
|
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/anacrolix/chansync v0.4.0 h1:Md0HM7zYCAO4KwNwgcIRgxNsMxiRuk7D1Ha0Uo+2y60=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
github.com/anacrolix/chansync v0.4.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
github.com/anacrolix/chansync v0.6.0 h1:/aQVvZ1yLRhmqEYrr9dC92JwzNBQ/SNnFi4uk+fTkQY=
|
||||||
github.com/anacrolix/dht/v2 v2.21.0 h1:8nzI+faaynY9jOKmVgdmBZVrTo8B7ZE/LKEgN3Vl/Bs=
|
github.com/anacrolix/chansync v0.6.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
||||||
github.com/anacrolix/dht/v2 v2.21.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
|
github.com/anacrolix/dht/v2 v2.22.0 h1:wat5FLdT25vltHsjX377GBrpK9o6L2QVn541bIguCYo=
|
||||||
|
github.com/anacrolix/dht/v2 v2.22.0/go.mod h1:shbBjhgvezqsJoE+hMo/ezHYQFF18V9jUllNIP5xV9k=
|
||||||
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||||
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||||
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
||||||
github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
|
github.com/anacrolix/envpprof v1.4.0 h1:QHeIcrgHcRChhnxR8l6rlaLlRQx9zd7Q2NII6Zbt83w=
|
||||||
github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
|
github.com/anacrolix/envpprof v1.4.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
|
||||||
github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so=
|
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca h1:aiiGqSQWjtVNdi8zUMfA//IrM8fPkv2bWwZVPbDe0wg=
|
||||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca/go.mod h1:MN3ve08Z3zSV/rTuX/ouI4lNdlfTxgdafQJiLzyNRB8=
|
||||||
github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
|
github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
|
||||||
github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
|
github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
|
||||||
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||||
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||||
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
|
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
|
||||||
github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
||||||
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 h1:CdVK9IoqoqklXQQ4+L2aew64xsz14KdOD+rnKdTQajg=
|
github.com/anacrolix/log v0.16.0 h1:DSuyb5kAJwl3Y0X1TRcStVrTS9ST9b0BHW+7neE4Xho=
|
||||||
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
github.com/anacrolix/log v0.16.0/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA=
|
||||||
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
|
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
|
||||||
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
|
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
|
||||||
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
|
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
|
||||||
@@ -62,30 +70,32 @@ github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy
|
|||||||
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
|
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
|
||||||
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
|
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
|
||||||
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
|
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3 h1:Ee//CmZBMadeNiYB/hHo9ly2PFOEZ4Fhsbnug3rDAIE=
|
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
|
github.com/anacrolix/missinggo/v2 v2.7.3/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.8.0 h1:6pGnVOlR6TWL9JM5Msyezij8YHU3+oHO7r82Eql/kpA=
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.8.0/go.mod h1:vVO5FEziQm+NFmJesc7StpkquZk+WJFCaL0Wp//2sa0=
|
||||||
github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
|
github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
|
||||||
github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
|
github.com/anacrolix/mmsg v1.1.1 h1:4ce/3I5kM7qSF6T5A8MOmDCfac3UqYlk5Bzh5XsWebM=
|
||||||
github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
|
github.com/anacrolix/mmsg v1.1.1/go.mod h1:lPCXEN1eDDQtKktdKEzdw+roswx6wWPpeXAl/WpWVDU=
|
||||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 h1:lOtCD+LzoD1g7bowhYJNR++uV+FyY5bTZXKwnPex9S8=
|
github.com/anacrolix/multiless v0.4.0 h1:lqSszHkliMsZd2hsyrDvHOw4AbYWa+ijQ66LzbjqWjM=
|
||||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
|
github.com/anacrolix/multiless v0.4.0/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
|
||||||
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
|
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
|
||||||
github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
|
github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
|
||||||
github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
|
github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
|
||||||
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
|
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
|
||||||
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
||||||
github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc=
|
github.com/anacrolix/sync v0.5.3 h1:iyMUiRRzTnD15+YQYxmDoobGdIbmUxdD4TNO67aCMYI=
|
||||||
github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
github.com/anacrolix/sync v0.5.3/go.mod h1:S51SPBetDsINK/KSNJtZkfM78s5rMeiO11/GNK6Rmh4=
|
||||||
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||||
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||||
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||||
github.com/anacrolix/torrent v1.53.3 h1:Abb3J8p/JWUjOzHaii8kixuDOGMXz+Oh8+hwc/zA2lA=
|
github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621 h1:h8YDIq0VGMrFVBqdd1FgBzF6AXbSaTyosk40++Ub9dE=
|
||||||
github.com/anacrolix/torrent v1.53.3/go.mod h1:is8GNob5qDeZ5Kq+pKPiE2xqYUi1ms7IgSB+CftZETk=
|
github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621/go.mod h1:n3SjHIE8oHXeH0Px0d5FXQ7cU4IgbEfTroen6B9KWJk=
|
||||||
github.com/anacrolix/upnp v0.1.3 h1:NlYEhE75adz2npEJKjbqyqnyW9qU4STookvSNXBJ5ao=
|
github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
|
||||||
github.com/anacrolix/upnp v0.1.3/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/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/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
@@ -98,13 +108,15 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
|||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/bits-and-blooms/bitset v1.15.0 h1:DiCRMscZsGyYePE9AR3sVhKqUXCt5IZvkX5AfAc5xLQ=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.15.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||||
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
|
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||||
@@ -117,18 +129,13 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
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=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
|
||||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05 h1:oK4+QcKsczZjHYTHD0JAdkvq5w74JEkG95J0XNBx/BI=
|
||||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c h1:hLoodLRD4KLWIH8eyAQCLcH8EqIrjac7fCkp/fHnvuQ=
|
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0=
|
|
||||||
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -136,9 +143,10 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
|
|||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
|
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
|
||||||
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
|
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||||
@@ -146,9 +154,10 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
|
|||||||
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||||
|
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
@@ -160,25 +169,29 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS
|
|||||||
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
|
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
|
||||||
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
|
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
|
||||||
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
|
||||||
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
|
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
|
||||||
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
|
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
github.com/go-llsqlite/adapter v0.1.0 h1:wGSQNsu/rtYeu/lqZNZQMjwUdEF3OW66xTLvsFwJQUw=
|
github.com/go-llsqlite/adapter v0.1.0 h1:wGSQNsu/rtYeu/lqZNZQMjwUdEF3OW66xTLvsFwJQUw=
|
||||||
github.com/go-llsqlite/adapter v0.1.0/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
|
github.com/go-llsqlite/adapter v0.1.0/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
|
||||||
github.com/go-llsqlite/crawshaw v0.5.0 h1:Olbqkth53vkkh4WvmkYjrtfOBcxXD3rMYBYuk6FNH3E=
|
github.com/go-llsqlite/crawshaw v0.5.5 h1:sXnRkiV26MBv++lbPbzp+ZzFcTqzVMxftO8yHyFvwUA=
|
||||||
github.com/go-llsqlite/crawshaw v0.5.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
|
github.com/go-llsqlite/crawshaw v0.5.5/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
|
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-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 v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
|
||||||
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=
|
||||||
@@ -197,28 +210,30 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
|||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.3/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
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-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
github.com/google/uuid v1.3.1/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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
@@ -229,29 +244,39 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
|
|||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
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.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||||
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
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/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/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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
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/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
|
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=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
@@ -270,82 +295,72 @@ github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
|||||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
|
||||||
|
github.com/mattn/go-ieproxy v0.0.12/go.mod h1:Vn+N61199DAnVeTgaF8eoB9PvLO8P3OBnG95ENh7B7c=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
|
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||||
|
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||||
|
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||||
|
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
|
||||||
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 v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
|
||||||
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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
|
||||||
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=
|
||||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
||||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
|
||||||
github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts=
|
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
|
||||||
github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
|
||||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
|
||||||
github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s=
|
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||||
github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
|
||||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
||||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
github.com/pion/webrtc/v4 v4.0.2 h1:fBwm5/hqSUybrCWl0DDBSTDrpbkcgkqpeLmXw9CsBQA=
|
||||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
github.com/pion/webrtc/v4 v4.0.2/go.mod h1:moylBT2A4dNoEaYBCdV1nThM3TLwRHzWszIG+eSPaqQ=
|
||||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
|
||||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
|
||||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
|
||||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
|
||||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
|
||||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
|
||||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
|
||||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
|
||||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
|
||||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
|
||||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
|
||||||
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
|
|
||||||
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
@@ -359,6 +374,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
|||||||
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=
|
||||||
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||||
|
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||||
|
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
@@ -366,18 +383,26 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
|
|||||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||||
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
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/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
||||||
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/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=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
|
||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
@@ -385,11 +410,11 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
|||||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
|
||||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
|
github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
|
||||||
github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
|
github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
|
||||||
@@ -397,32 +422,35 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
|
|||||||
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
|
||||||
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/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=
|
||||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjnw+Ylb2vE27Gtqb7BDmfR+nLcJwvbJh98U=
|
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459/go.mod h1:nFvuG3SWu3VWqobG3cX8nt57wXU0OOFapeCs/8axIuM=
|
|
||||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
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=
|
||||||
@@ -430,17 +458,19 @@ github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgK
|
|||||||
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=
|
||||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
|
||||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
|
||||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
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=
|
||||||
@@ -449,17 +479,12 @@ 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.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
|
||||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
|
||||||
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-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
@@ -469,8 +494,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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
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=
|
||||||
@@ -482,26 +507,18 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
|
||||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|
||||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
|
||||||
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=
|
||||||
@@ -511,11 +528,12 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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-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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
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=
|
||||||
@@ -525,19 +543,18 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -545,50 +562,32 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.10.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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.27.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.1.0/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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|
||||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
|
||||||
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=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
|
||||||
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.9.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.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
|
||||||
golang.org/x/time v0.5.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=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -596,12 +595,12 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
|
|||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
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.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||||
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=
|
||||||
@@ -622,7 +621,6 @@ 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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
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=
|
||||||
@@ -646,13 +644,58 @@ 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=
|
||||||
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=
|
||||||
modernc.org/libc v1.40.6 h1:141JHq3SjhOOCjECBgD4K8VgTFOy19CnHwroC08DAig=
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
modernc.org/libc v1.40.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
|
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
|
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||||
|
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
|
||||||
|
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||||
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
|
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
|
||||||
|
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
|
||||||
|
modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
|
||||||
|
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||||
|
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||||
|
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
|
||||||
|
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||||
|
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||||
|
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
|
||||||
|
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
|
||||||
|
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||||
|
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
|
||||||
|
modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
|
||||||
|
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
|
||||||
|
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
||||||
|
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
zombiezen.com/go/sqlite v1.1.0 h1:2BIABTFCE221++xR90rV0mE6T5gFF8o0MUcKDNUrIVc=
|
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
zombiezen.com/go/sqlite v1.1.0/go.mod h1:rZEFEJSoeXeiwuAb6HugOJLhtDabl+EekT92DCEKOb0=
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
|
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
|
||||||
|
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||||
|
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||||
|
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
|
||||||
|
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
||||||
|
zombiezen.com/go/sqlite v0.12.0/go.mod h1:RKdRR9xoQDSnB47yy7G4PtrjGZJtupb/SyEbJZLaRes=
|
||||||
|
zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
|
||||||
|
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
GetConfig func(v any) bool
|
GetConfig func(v any)
|
||||||
ProxyUrl *url.URL
|
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
||||||
FileController
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
}
|
}
|
||||||
@@ -22,10 +24,9 @@ type DefaultFileController struct {
|
|||||||
|
|
||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
|
GetConfig: func(v any) {},
|
||||||
|
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
GetConfig: func(v any) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetcher defines the interface for a download protocol.
|
// Fetcher defines the interface for a download protocol.
|
||||||
// One fetcher for each download task
|
// Each download task will have a corresponding Fetcher instance for the management of the download task
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
// Name return the name of the protocol.
|
|
||||||
Name() string
|
|
||||||
|
|
||||||
Setup(ctl *controller.Controller)
|
Setup(ctl *controller.Controller)
|
||||||
// Resolve resource info from request
|
// Resolve resource info from request
|
||||||
Resolve(req *base.Request) error
|
Resolve(req *base.Request) error
|
||||||
@@ -31,6 +29,12 @@ type Fetcher interface {
|
|||||||
Wait() error
|
Wait() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Uploader interface {
|
||||||
|
Upload() error
|
||||||
|
UploadedBytes() int64
|
||||||
|
WaitUpload() error
|
||||||
|
}
|
||||||
|
|
||||||
// FetcherMeta defines the meta information of a fetcher.
|
// FetcherMeta defines the meta information of a fetcher.
|
||||||
type FetcherMeta struct {
|
type FetcherMeta struct {
|
||||||
Req *base.Request `json:"req"`
|
Req *base.Request `json:"req"`
|
||||||
@@ -68,17 +72,57 @@ func (m *FetcherMeta) RootDirPath() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetcherBuilder defines the interface for a fetcher builder.
|
type FilterType int
|
||||||
type FetcherBuilder interface {
|
|
||||||
// Schemes returns the schemes supported by the fetcher.
|
const (
|
||||||
Schemes() []string
|
// FilterTypeUrl url type, pattern is the scheme, e.g. http://github.com -> http
|
||||||
|
FilterTypeUrl FilterType = iota
|
||||||
|
// FilterTypeFile file type, pattern is the file extension name, e.g. test.torrent -> torrent
|
||||||
|
FilterTypeFile
|
||||||
|
// FilterTypeBase64 base64 data type, pattern is the data mime type, e.g. data:application/x-bittorrent;base64 -> application/x-bittorrent
|
||||||
|
FilterTypeBase64
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchemeFilter struct {
|
||||||
|
Type FilterType
|
||||||
|
Pattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SchemeFilter) Match(uri string) bool {
|
||||||
|
uriUpper := strings.ToUpper(uri)
|
||||||
|
patternUpper := strings.ToUpper(s.Pattern)
|
||||||
|
switch s.Type {
|
||||||
|
case FilterTypeUrl:
|
||||||
|
return strings.HasPrefix(uriUpper, patternUpper+":")
|
||||||
|
case FilterTypeFile:
|
||||||
|
return strings.HasSuffix(uriUpper, "."+patternUpper)
|
||||||
|
case FilterTypeBase64:
|
||||||
|
return strings.HasPrefix(uriUpper, "DATA:"+patternUpper+";BASE64,")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetcherManager manage and control the fetcher
|
||||||
|
type FetcherManager interface {
|
||||||
|
// Name return the name of the protocol.
|
||||||
|
Name() string
|
||||||
|
// Filters registers the supported schemes.
|
||||||
|
Filters() []*SchemeFilter
|
||||||
// Build returns a new fetcher.
|
// Build returns a new fetcher.
|
||||||
Build() Fetcher
|
Build() Fetcher
|
||||||
|
// ParseName name displayed when the task is not yet resolved, parsed from the request URL
|
||||||
|
ParseName(u string) string
|
||||||
|
// AutoRename returns whether the fetcher need renaming the download file when has the same name file.
|
||||||
|
AutoRename() bool
|
||||||
|
|
||||||
|
// DefaultConfig returns the default configuration of the protocol.
|
||||||
|
DefaultConfig() any
|
||||||
// Store fetcher
|
// Store fetcher
|
||||||
Store(fetcher Fetcher) (any, error)
|
Store(fetcher Fetcher) (any, error)
|
||||||
// Restore fetcher
|
// Restore fetcher
|
||||||
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
||||||
|
// Close the fetcher manager, release resources.
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultFetcher struct {
|
type DefaultFetcher struct {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package fetcher
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSchemeFilter_Match(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Type FilterType
|
||||||
|
Pattern string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
uri string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "url match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeUrl,
|
||||||
|
Pattern: "https",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "https://github.com",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url not match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeUrl,
|
||||||
|
Pattern: "https",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "ftp://github.com",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeFile,
|
||||||
|
Pattern: "torrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "d:/temp/test.torrent",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file not match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeFile,
|
||||||
|
Pattern: "torrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "d:/temp/test.txt",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base64 match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeBase64,
|
||||||
|
Pattern: "application/x-bittorrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "data:application/x-bittorrent;base64,xxx",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base64 not match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeBase64,
|
||||||
|
Pattern: "application/x-bittorrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "data:application/javascript;base64,xxx",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &SchemeFilter{
|
||||||
|
Type: tt.fields.Type,
|
||||||
|
Pattern: tt.fields.Pattern,
|
||||||
|
}
|
||||||
|
if got := s.Match(tt.args.uri); got != tt.want {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,4 +3,10 @@ package bt
|
|||||||
type config struct {
|
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 bool `json:"seedKeep`
|
||||||
|
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||||
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
|
SeedTime int64 `json:"seedTime"`
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/dnscache"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DnsCacheResolver resolves DNS requests for an HTTP client using an in-memory cache.
|
||||||
|
type DnsCacheResolver struct {
|
||||||
|
RefreshTimeout time.Duration
|
||||||
|
|
||||||
|
resolver dnscache.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DnsCacheResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ips, err := r.resolver.LookupHost(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var conn net.Conn
|
||||||
|
for _, ip := range ips {
|
||||||
|
var dialer net.Dialer
|
||||||
|
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip, port))
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DnsCacheResolver) Run(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(r.RefreshTimeout)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
r.resolver.Refresh(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+313
-96
@@ -2,6 +2,7 @@ package bt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
@@ -10,7 +11,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
"net/http"
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -23,6 +26,8 @@ var (
|
|||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
torrentDirMap = make(map[string]string)
|
torrentDirMap = make(map[string]string)
|
||||||
ftMap = make(map[string]*fileTorrentImpl)
|
ftMap = make(map[string]*fileTorrentImpl)
|
||||||
|
closeCtx context.Context
|
||||||
|
closeFunc func()
|
||||||
)
|
)
|
||||||
|
|
||||||
type Fetcher struct {
|
type Fetcher struct {
|
||||||
@@ -31,15 +36,13 @@ type Fetcher struct {
|
|||||||
|
|
||||||
torrent *torrent.Torrent
|
torrent *torrent.Torrent
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
|
data *fetcherData
|
||||||
|
|
||||||
torrentReady atomic.Bool
|
torrentReady atomic.Bool
|
||||||
torrentDrop atomic.Bool
|
torrentUpload atomic.Bool
|
||||||
create atomic.Bool
|
torrentDropCtx context.Context
|
||||||
progress fetcher.Progress
|
torrentDropFunc func()
|
||||||
}
|
uploadDoneCh chan any
|
||||||
|
|
||||||
func (f *Fetcher) Name() string {
|
|
||||||
return "bt"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||||
@@ -47,13 +50,12 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
|||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
exist := f.ctl.GetConfig(&f.config)
|
if f.data == nil {
|
||||||
if !exist {
|
f.data = &fetcherData{}
|
||||||
f.config = &config{
|
|
||||||
ListenPort: 0,
|
|
||||||
Trackers: []string{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
f.uploadDoneCh = make(chan any, 1)
|
||||||
|
f.torrentDropCtx, f.torrentDropFunc = context.WithCancel(context.Background())
|
||||||
|
f.ctl.GetConfig(&f.config)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,14 +66,16 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
if client != nil {
|
if client != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if closeCtx == nil {
|
||||||
|
closeCtx, closeFunc = context.WithCancel(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
cfg := torrent.NewDefaultClientConfig()
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.Seed = true
|
||||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||||
cfg.ListenPort = f.config.ListenPort
|
cfg.ListenPort = f.config.ListenPort
|
||||||
if f.ctl.ProxyUrl != nil {
|
cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
|
||||||
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
|
|
||||||
}
|
|
||||||
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||||
ClientBaseDir: cfg.DataDir,
|
ClientBaseDir: cfg.DataDir,
|
||||||
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||||
@@ -81,7 +85,17 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
ftMap[infoHash.String()] = ft
|
ftMap[infoHash.String()] = ft
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
dnsResolver := &DnsCacheResolver{RefreshTimeout: 5 * time.Minute}
|
||||||
|
cfg.TrackerDialContext = dnsResolver.DialContext
|
||||||
client, err = torrent.NewClient(cfg)
|
client, err = torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCtx, closeFunc = context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
dnsResolver.Run(closeCtx)
|
||||||
|
}()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,42 +103,42 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
f.meta.Req = req
|
||||||
if err := f.addTorrent(req); err != nil {
|
if err := f.addTorrent(req, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.updateRes()
|
f.updateRes()
|
||||||
f.meta.Req = req
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
f.create.Store(true)
|
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
if f.meta.Res != nil {
|
if f.meta.Res != nil {
|
||||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
torrentDirMap[f.meta.Res.Hash] = opts.Path
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Start() (err error) {
|
func (f *Fetcher) Start() (err error) {
|
||||||
if !f.torrentReady.Load() {
|
if !f.torrentReady.Load() {
|
||||||
if err = f.addTorrent(f.meta.Req); err != nil {
|
if err = f.addTorrent(f.meta.Req, false); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||||
ft.setTorrentDir(f.meta.FolderPath())
|
ft.setTorrentDir(f.meta.Opts.Path)
|
||||||
}
|
}
|
||||||
files := f.torrent.Files()
|
files := f.torrent.Files()
|
||||||
// If the user does not specify the file to download, all files will be downloaded by default
|
// If the user does not specify the file to download, all files will be downloaded by default
|
||||||
if len(f.meta.Opts.SelectFiles) == 0 {
|
if f.data.Progress == nil {
|
||||||
f.meta.Opts.SelectFiles = make([]int, len(files))
|
if len(f.meta.Opts.SelectFiles) == 0 {
|
||||||
for i := range files {
|
f.meta.Opts.SelectFiles = make([]int, len(files))
|
||||||
f.meta.Opts.SelectFiles[i] = i
|
for i := range files {
|
||||||
|
f.meta.Opts.SelectFiles[i] = i
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
f.data.Progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
}
|
}
|
||||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
|
||||||
if len(f.meta.Opts.SelectFiles) == len(files) {
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||||
f.torrent.DownloadAll()
|
f.torrent.DownloadAll()
|
||||||
} else {
|
} else {
|
||||||
@@ -133,7 +147,6 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
file.Download()
|
file.Download()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +157,12 @@ func (f *Fetcher) Pause() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Close() (err error) {
|
func (f *Fetcher) Close() (err error) {
|
||||||
f.torrentDrop.Store(false)
|
|
||||||
f.safeDrop()
|
f.safeDrop()
|
||||||
|
f.torrentDropFunc()
|
||||||
|
f.uploadDoneCh <- nil
|
||||||
|
if len(client.Torrents()) == 0 {
|
||||||
|
err = closeClient()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,80 +175,94 @@ func (f *Fetcher) safeDrop() {
|
|||||||
f.torrent.Drop()
|
f.torrent.Drop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Wait() (err error) {
|
|
||||||
for {
|
|
||||||
if f.torrentDrop.Load() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
|
||||||
done := true
|
|
||||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
|
||||||
file := f.torrent.Files()[selectIndex]
|
|
||||||
if file.BytesCompleted() < file.Length() {
|
|
||||||
done = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if done {
|
|
||||||
// remove unselected files
|
|
||||||
for i, file := range f.torrent.Files() {
|
|
||||||
selected := false
|
|
||||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
|
||||||
if i == selectIndex {
|
|
||||||
selected = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !selected {
|
|
||||||
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
time.Sleep(time.Millisecond * 500)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||||
return f.meta
|
return f.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Stats() any {
|
func (f *Fetcher) Stats() any {
|
||||||
stats := f.torrent.Stats()
|
var stats torrent.TorrentStats
|
||||||
baseStats := &bt.Stats{
|
if f.torrent != nil {
|
||||||
|
stats = f.torrent.Stats()
|
||||||
|
} else {
|
||||||
|
stats = torrent.TorrentStats{}
|
||||||
|
}
|
||||||
|
return &bt.Stats{
|
||||||
TotalPeers: stats.TotalPeers,
|
TotalPeers: stats.TotalPeers,
|
||||||
ActivePeers: stats.ActivePeers,
|
ActivePeers: stats.ActivePeers,
|
||||||
ConnectedSeeders: stats.ConnectedSeeders,
|
ConnectedSeeders: stats.ConnectedSeeders,
|
||||||
|
SeedBytes: f.data.SeedBytes,
|
||||||
|
SeedRatio: f.seedRadio(),
|
||||||
|
SeedTime: f.data.SeedTime,
|
||||||
}
|
}
|
||||||
return baseStats
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
if !f.torrentReady.Load() {
|
if !f.torrentReady.Load() {
|
||||||
return f.progress
|
return f.data.Progress
|
||||||
}
|
}
|
||||||
for i := range f.progress {
|
for i := range f.data.Progress {
|
||||||
selectIndex := f.meta.Opts.SelectFiles[i]
|
selectIndex := f.meta.Opts.SelectFiles[i]
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
f.progress[i] = file.BytesCompleted()
|
f.data.Progress[i] = file.BytesCompleted()
|
||||||
}
|
}
|
||||||
return f.progress
|
return f.data.Progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Wait() (err error) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.torrentDropCtx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
||||||
|
if f.isDone() {
|
||||||
|
// remove unselected files
|
||||||
|
for i, file := range f.torrent.Files() {
|
||||||
|
selected := false
|
||||||
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
|
if i == selectIndex {
|
||||||
|
selected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !selected {
|
||||||
|
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) isDone() bool {
|
||||||
|
if f.meta.Opts == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
|
file := f.torrent.Files()[selectIndex]
|
||||||
|
if file.BytesCompleted() < file.Length() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) updateRes() {
|
func (f *Fetcher) updateRes() {
|
||||||
res := &base.Resource{
|
res := &base.Resource{
|
||||||
Name: f.torrent.Name(),
|
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||||
Hash: f.torrent.InfoHash().String(),
|
Hash: f.torrent.InfoHash().String(),
|
||||||
}
|
}
|
||||||
f.torrent.PeerConns()
|
// Directory torrent
|
||||||
|
if f.torrent.Info().Length == 0 {
|
||||||
|
res.Name = f.torrent.Name()
|
||||||
|
}
|
||||||
for i, file := range f.torrent.Files() {
|
for i, file := range f.torrent.Files() {
|
||||||
res.Files[i] = &base.FileInfo{
|
res.Files[i] = &base.FileInfo{
|
||||||
Name: filepath.Base(file.DisplayPath()),
|
Name: filepath.Base(file.DisplayPath()),
|
||||||
Path: util.Dir(file.Path()),
|
Path: util.Dir(file.DisplayPath()),
|
||||||
Size: file.Length(),
|
Size: file.Length(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,24 +273,115 @@ func (f *Fetcher) updateRes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
func (f *Fetcher) Upload() (err error) {
|
||||||
|
return f.addTorrent(f.meta.Req, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) doUpload(fromUpload bool) {
|
||||||
|
if !f.torrentUpload.CompareAndSwap(false, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and update seed data
|
||||||
|
lastData := &fetcherData{
|
||||||
|
SeedBytes: f.data.SeedBytes,
|
||||||
|
SeedTime: f.data.SeedTime,
|
||||||
|
}
|
||||||
|
var doneTime int64 = 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.torrentDropCtx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
if !f.torrentReady.Load() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := f.torrentStats()
|
||||||
|
f.data.SeedBytes = lastData.SeedBytes + stats.BytesWrittenData.Int64()
|
||||||
|
|
||||||
|
// Check is download complete, if not don't check and stop seeding
|
||||||
|
if !fromUpload && !f.isDone() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if doneTime == 0 {
|
||||||
|
doneTime = time.Now().Unix()
|
||||||
|
}
|
||||||
|
f.data.SeedTime = lastData.SeedTime + time.Now().Unix() - doneTime
|
||||||
|
|
||||||
|
// If the seed forever is true, keep seeding
|
||||||
|
if f.config.SeedKeep {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the seed ratio is reached, stop seeding
|
||||||
|
if f.config.SeedRatio > 0 {
|
||||||
|
seedRadio := f.seedRadio()
|
||||||
|
if seedRadio >= f.config.SeedRatio {
|
||||||
|
f.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the seed time is reached, stop seeding
|
||||||
|
if f.config.SeedTime > 0 {
|
||||||
|
if f.data.SeedTime >= f.config.SeedTime {
|
||||||
|
f.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get torrent stats maybe panic, see https://github.com/anacrolix/torrent/issues/972
|
||||||
|
func (f *Fetcher) torrentStats() torrent.TorrentStats {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// ignore panic
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return f.torrent.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) UploadedBytes() int64 {
|
||||||
|
return f.data.SeedBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) WaitUpload() (err error) {
|
||||||
|
<-f.uploadDoneCh
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||||
if err = f.initClient(); err != nil {
|
if err = f.initClient(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
schema := util.ParseSchema(req.URL)
|
schema := util.ParseSchema(req.URL)
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(req.URL)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else if schema == "APPLICATION/X-BITTORRENT" {
|
} else {
|
||||||
_, data := util.ParseDataUri(req.URL)
|
var reader io.Reader
|
||||||
buf := bytes.NewBuffer(data)
|
if schema == "DATA" {
|
||||||
|
_, data := util.ParseDataUri(req.URL)
|
||||||
|
reader = bytes.NewBuffer(data)
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(req.URL)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.(io.Closer).Close()
|
||||||
|
}
|
||||||
|
|
||||||
var metaInfo *metainfo.MetaInfo
|
var metaInfo *metainfo.MetaInfo
|
||||||
metaInfo, err = metainfo.Load(buf)
|
metaInfo, err = metainfo.Load(reader)
|
||||||
if err != nil {
|
// Hotfix for https://github.com/anacrolix/torrent/issues/992, ignore "expected EOF" error
|
||||||
|
// TODO remove this after the issue is fixed
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.torrent, err = client.AddTorrent(metaInfo)
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
} else {
|
|
||||||
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -289,34 +411,129 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
|||||||
}
|
}
|
||||||
<-f.torrent.GotInfo()
|
<-f.torrent.GotInfo()
|
||||||
f.torrentReady.Store(true)
|
f.torrentReady.Store(true)
|
||||||
|
|
||||||
|
go f.doUpload(fromUpload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherBuilder struct {
|
func (f *Fetcher) seedRadio() float64 {
|
||||||
|
var bytesRead int64
|
||||||
|
if f.Meta().Res != nil {
|
||||||
|
bytesRead = f.Meta().Res.Size
|
||||||
|
} else {
|
||||||
|
bytesRead = 0
|
||||||
|
}
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return float64(f.data.SeedBytes) / float64(bytesRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
|
type fetcherData struct {
|
||||||
|
Progress fetcher.Progress
|
||||||
func (fb *FetcherBuilder) Schemes() []string {
|
SeedBytes int64
|
||||||
return schemes
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
|
SeedTime int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
func closeClient() error {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
if closeFunc != nil {
|
||||||
|
closeFunc()
|
||||||
|
}
|
||||||
|
if client != nil {
|
||||||
|
errs := client.Close()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errs[0]
|
||||||
|
}
|
||||||
|
client = nil
|
||||||
|
closeCtx = nil
|
||||||
|
closeFunc = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetcherManager struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Name() string {
|
||||||
|
return "bt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||||
|
return []*fetcher.SchemeFilter{
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeUrl,
|
||||||
|
Pattern: "MAGNET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeFile,
|
||||||
|
Pattern: "TORRENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeBase64,
|
||||||
|
Pattern: "APPLICATION/X-BITTORRENT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
return nil, nil
|
var name string
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
params := url.Query()
|
||||||
|
if params.Get("dn") != "" {
|
||||||
|
return params.Get("dn")
|
||||||
|
}
|
||||||
|
if params.Get("xt") != "" {
|
||||||
|
xt := strings.Split(params.Get("xt"), ":")
|
||||||
|
return xt[len(xt)-1]
|
||||||
|
}
|
||||||
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
|
return &config{
|
||||||
|
ListenPort: 0,
|
||||||
|
Trackers: []string{},
|
||||||
|
SeedKeep: false,
|
||||||
|
SeedRatio: 1.0,
|
||||||
|
SeedTime: 120 * 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
|
_f := f.(*Fetcher)
|
||||||
|
return _f.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
|
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
return &Fetcher{
|
return &Fetcher{
|
||||||
meta: meta,
|
meta: meta,
|
||||||
|
data: v.(*fetcherData),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Close() error {
|
||||||
|
return closeClient()
|
||||||
|
}
|
||||||
|
|
||||||
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
||||||
func parseBep20() string {
|
func parseBep20() string {
|
||||||
s := strings.ReplaceAll(base.Version, ".", "")
|
s := strings.ReplaceAll(base.Version, ".", "")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -35,7 +35,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
|
||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -60,61 +59,171 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
|||||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
|
doResolve(t, buildConfigFetcher(&base.DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
System: false,
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
t.Run("Resolve Single File", func(t *testing.T) {
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
err := fetcher.Resolve(&base.Request{
|
||||||
Extra: bt.ReqExtra{
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
Trackers: []string{
|
Extra: bt.ReqExtra{
|
||||||
"udp://tracker.birkenwald.de:6969/announce",
|
Trackers: []string{
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
panic(err)
|
||||||
panic(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Size: 1466714112,
|
||||||
Size: 1466714112,
|
Range: true,
|
||||||
Range: true,
|
Files: []*base.FileInfo{
|
||||||
Files: []*base.FileInfo{
|
{
|
||||||
{
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Size: 1466714112,
|
||||||
Size: 1466714112,
|
},
|
||||||
},
|
},
|
||||||
},
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve Single File Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Resolve Multi Files", func(t *testing.T) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/test.torrent",
|
||||||
|
Extra: bt.ReqExtra{
|
||||||
|
Trackers: []string{
|
||||||
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &base.Resource{
|
||||||
|
Name: "test",
|
||||||
|
Size: 107484864,
|
||||||
|
Range: true,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "c.txt",
|
||||||
|
Path: "path",
|
||||||
|
Size: 98501754,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "b.txt",
|
||||||
|
Size: 8904996,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "a.txt",
|
||||||
|
Size: 78114,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve Multi Files Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/test.unclean.torrent",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
tests := []struct {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken url",
|
||||||
|
args: args{
|
||||||
|
u: "magnet://!@#%github.com",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dn",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed&dn=ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
},
|
||||||
|
want: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no dn",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
},
|
||||||
|
want: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non standard magnet",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xxt=abcd",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||||
|
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFetcher() fetcher.Fetcher {
|
func buildFetcher() fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fb := new(FetcherManager)
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher := fb.Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
newController.GetConfig = func(v any) {
|
||||||
|
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
|
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherManager).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
mockCfg := config{
|
mockCfg := config{
|
||||||
Trackers: []string{
|
Trackers: []string{
|
||||||
"udp://tracker.birkenwald.de:6969/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
}}
|
}}
|
||||||
newController.GetConfig = func(v any) bool {
|
newController.GetConfig = func(v any) {
|
||||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
||||||
return false
|
}
|
||||||
}
|
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
return true
|
return proxyConfig.ToHandler()
|
||||||
}
|
}
|
||||||
newController.ProxyUrl = proxyUrl
|
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/anacrolix/torrent/storage"
|
"github.com/anacrolix/torrent/storage"
|
||||||
"io"
|
"io"
|
||||||
@@ -53,7 +54,7 @@ func (fs fileClientImpl) Close() error {
|
|||||||
return fs.opts.PieceCompletion.Close()
|
return fs.opts.PieceCompletion.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
func (fs fileClientImpl) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
||||||
upvertedFiles := info.UpvertedFiles()
|
upvertedFiles := info.UpvertedFiles()
|
||||||
files := make([]file, 0, len(upvertedFiles))
|
files := make([]file, 0, len(upvertedFiles))
|
||||||
for _, fileInfo := range upvertedFiles {
|
for _, fileInfo := range upvertedFiles {
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -3,16 +3,12 @@ package http
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
|
||||||
"github.com/xiaoqidun/setft"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -21,8 +17,18 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/xiaoqidun/setft"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const connectTimeout = 15 * time.Second
|
||||||
|
const readTimeout = 15 * time.Second
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
Code int
|
Code int
|
||||||
Msg string
|
Msg string
|
||||||
@@ -64,23 +70,13 @@ type Fetcher struct {
|
|||||||
eg *errgroup.Group
|
eg *errgroup.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Name() string {
|
|
||||||
return "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||||
f.ctl = ctl
|
f.ctl = ctl
|
||||||
f.doneCh = make(chan error, 1)
|
f.doneCh = make(chan error, 1)
|
||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
exist := f.ctl.GetConfig(&f.config)
|
f.ctl.GetConfig(&f.config)
|
||||||
if !exist {
|
|
||||||
f.config = &config{
|
|
||||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
|
||||||
Connections: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +84,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
f.meta.Req = req
|
||||||
httpReq, err := f.buildRequest(nil, req)
|
httpReq, err := f.buildRequest(nil, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -108,9 +105,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
||||||
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
|
// response 206 status code, support breakpoint continuation
|
||||||
res.Range = true
|
res.Range = true
|
||||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
// parse content length from Content-Range header, eg: bytes 0-1000/1001
|
||||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||||
if contentTotal != "" {
|
if contentTotal != "" {
|
||||||
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
||||||
@@ -120,7 +117,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
res.Size = parse
|
res.Size = parse
|
||||||
}
|
}
|
||||||
} else if base.HttpCodeOK == httpResp.StatusCode {
|
} else if base.HttpCodeOK == httpResp.StatusCode {
|
||||||
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
// response 200 status code, not support breakpoint continuation, get file size by Content-Length header
|
||||||
|
// if not found, maybe chunked encoding
|
||||||
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||||
@@ -152,7 +150,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
file.Name = filename
|
file.Name = filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get file filePath by URL
|
// get file filePath by URL
|
||||||
if file.Name == "" {
|
if file.Name == "" {
|
||||||
file.Name = path.Base(httpReq.URL.Path)
|
file.Name = path.Base(httpReq.URL.Path)
|
||||||
}
|
}
|
||||||
@@ -161,7 +159,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
file.Name = httpReq.URL.Hostname()
|
file.Name = httpReq.URL.Hostname()
|
||||||
}
|
}
|
||||||
res.Files = append(res.Files, file)
|
res.Files = append(res.Files, file)
|
||||||
f.meta.Req = req
|
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -169,9 +166,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
func (f *Fetcher) Create(opts *base.Options) error {
|
func (f *Fetcher) Create(opts *base.Options) error {
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
|
|
||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -179,8 +173,12 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
|||||||
opts.Extra = &fhttp.OptsExtra{}
|
opts.Extra = &fhttp.OptsExtra{}
|
||||||
}
|
}
|
||||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||||
if extra.Connections == 0 {
|
if extra.Connections <= 0 {
|
||||||
extra.Connections = f.config.Connections
|
extra.Connections = f.config.Connections
|
||||||
|
// Avoid zero connections configuration
|
||||||
|
if extra.Connections <= 0 {
|
||||||
|
extra.Connections = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -201,6 +199,12 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid request extra modified by extension
|
||||||
|
if err = base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if f.chunks == nil {
|
if f.chunks == nil {
|
||||||
f.chunks = f.splitChunk()
|
f.chunks = f.splitChunk()
|
||||||
}
|
}
|
||||||
@@ -250,23 +254,42 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
return <-f.doneCh
|
return <-f.doneCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fetchResult struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) fetch() {
|
func (f *Fetcher) fetch() {
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
ctx, f.cancel = context.WithCancel(context.Background())
|
ctx, f.cancel = context.WithCancel(context.Background())
|
||||||
f.eg, _ = errgroup.WithContext(ctx)
|
f.eg, _ = errgroup.WithContext(ctx)
|
||||||
|
chunkErrs := make([]error, len(f.chunks))
|
||||||
for i := 0; i < len(f.chunks); i++ {
|
for i := 0; i < len(f.chunks); i++ {
|
||||||
i := i
|
i := i
|
||||||
f.eg.Go(func() error {
|
f.eg.Go(func() error {
|
||||||
return f.fetchChunk(i, ctx)
|
err := f.fetchChunk(i, ctx)
|
||||||
|
// if canceled, fail fast
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chunkErrs[i] = err
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := f.eg.Wait()
|
err := f.eg.Wait()
|
||||||
// check if canceled
|
// error returned only if canceled, just return
|
||||||
if errors.Is(err, context.Canceled) {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// check all fetch results, if any error, return
|
||||||
|
for _, chunkErr := range chunkErrs {
|
||||||
|
if chunkErr != nil {
|
||||||
|
err = chunkErr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
f.file.Close()
|
f.file.Close()
|
||||||
// Update file last modified time
|
// Update file last modified time
|
||||||
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
||||||
@@ -289,24 +312,11 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
for {
|
for {
|
||||||
// if chunk is completed, return
|
// if chunk is completed, return
|
||||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if chunk.retryTimes >= maxRetries {
|
if chunk.retryTimes >= maxRetries {
|
||||||
if !f.meta.Res.Range {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if all failed
|
|
||||||
allFailed := true
|
|
||||||
for _, c := range f.chunks {
|
|
||||||
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
|
||||||
allFailed = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allFailed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -333,10 +343,9 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Http request success, reset retry times
|
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||||
chunk.retryTimes = 0
|
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := reader.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -351,7 +360,6 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If canceled, do not retry
|
// If canceled, do not retry
|
||||||
@@ -386,7 +394,8 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
method string
|
method string
|
||||||
body io.Reader
|
body io.Reader
|
||||||
)
|
)
|
||||||
headers := make(map[string][]string)
|
|
||||||
|
headers := http.Header{}
|
||||||
if req.Extra == nil {
|
if req.Extra == nil {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
} else {
|
} else {
|
||||||
@@ -398,7 +407,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
if len(extra.Header) > 0 {
|
if len(extra.Header) > 0 {
|
||||||
for k, v := range extra.Header {
|
for k, v := range extra.Header {
|
||||||
headers[k] = []string{v}
|
headers.Set(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if extra.Body != "" {
|
if extra.Body != "" {
|
||||||
@@ -406,8 +415,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||||
// load user agent from config
|
headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
|
||||||
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
@@ -451,12 +459,17 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) buildClient() *http.Client {
|
func (f *Fetcher) buildClient() *http.Client {
|
||||||
transport := &http.Transport{}
|
transport := &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: connectTimeout,
|
||||||
|
}).DialContext,
|
||||||
|
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
||||||
|
},
|
||||||
|
}
|
||||||
// Cookie handle
|
// Cookie handle
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
if f.ctl.ProxyUrl != nil {
|
|
||||||
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
|
|
||||||
}
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Jar: jar,
|
Jar: jar,
|
||||||
@@ -467,30 +480,67 @@ type fetcherData struct {
|
|||||||
Chunks []*chunk
|
Chunks []*chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherBuilder struct {
|
type FetcherManager struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var schemes = []string{"HTTP", "HTTPS"}
|
func (fm *FetcherManager) Name() string {
|
||||||
|
return "http"
|
||||||
func (fb *FetcherBuilder) Schemes() []string {
|
|
||||||
return schemes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||||
|
return []*fetcher.SchemeFilter{
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeUrl,
|
||||||
|
Pattern: "HTTP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeUrl,
|
||||||
|
Pattern: "HTTPS",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
|
var name string
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Get filePath by URL
|
||||||
|
name = path.Base(url.Path)
|
||||||
|
// If file name is empty, use host name
|
||||||
|
if name == "" || name == "/" || name == "." {
|
||||||
|
name = url.Hostname()
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
|
return &config{
|
||||||
|
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||||
|
Connections: 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
_f := f.(*Fetcher)
|
_f := f.(*Fetcher)
|
||||||
return &fetcherData{
|
return &fetcherData{
|
||||||
Chunks: _f.chunks,
|
Chunks: _f.chunks,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
fd := v.(*fetcherData)
|
fd := v.(*fetcherData)
|
||||||
fb := &FetcherBuilder{}
|
fb := &FetcherManager{}
|
||||||
fetcher := fb.Build().(*Fetcher)
|
fetcher := fb.Build().(*Fetcher)
|
||||||
fetcher.meta = meta
|
fetcher.meta = meta
|
||||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
@@ -501,3 +551,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -8,8 +8,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
"net"
|
"net"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -131,6 +132,14 @@ func TestFetcher_DownloadLimit(t *testing.T) {
|
|||||||
downloadNormal(listener, 8, t)
|
downloadNormal(listener, 8, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
|
||||||
|
listener := test.StartTestLimitServer(16, readTimeout.Milliseconds()+5000)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadError(listener, 1, t)
|
||||||
|
downloadError(listener, 4, t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadResume(t *testing.T) {
|
func TestFetcher_DownloadResume(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -193,6 +202,54 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken url",
|
||||||
|
args: args{
|
||||||
|
u: "https://!@#%github.com",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/index.html",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path with query and hash",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/a/b/index.html/#list?name=1",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com",
|
||||||
|
},
|
||||||
|
want: "github.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||||
|
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||||
return doDownloadReady(buildFetcher(), listener, connections, t)
|
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||||
}
|
}
|
||||||
@@ -307,7 +364,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fb := new(FetcherBuilder)
|
fb := new(FetcherManager)
|
||||||
time.Sleep(time.Millisecond * 50)
|
time.Sleep(time.Millisecond * 50)
|
||||||
data, err := fb.Store(fetcher)
|
data, err := fb.Store(fetcher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -338,7 +395,13 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||||
fetcher := downloadReady(httpListener, 4, t)
|
fetcher := downloadReady(httpListener, 4, t)
|
||||||
ctl := controller.NewController()
|
ctl := controller.NewController()
|
||||||
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
return (&base.DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
}).ToHandler()
|
||||||
|
}
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -356,19 +419,21 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildFetcher() *Fetcher {
|
func buildFetcher() *Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fm := new(FetcherManager)
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher := fm.Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
newController.GetConfig = func(v any) {
|
||||||
|
json.Unmarshal([]byte(test.ToJson(fm.DefaultConfig())), v)
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
return fetcher.(*Fetcher)
|
return fetcher.(*Fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherManager).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
newController.GetConfig = func(v any) bool {
|
newController.GetConfig = func(v any) {
|
||||||
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
json.Unmarshal([]byte(test.ToJson(cfg)), v)
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeoutReader struct {
|
||||||
|
reader io.Reader
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeoutReader(r io.Reader, timeout time.Duration) *TimeoutReader {
|
||||||
|
return &TimeoutReader{
|
||||||
|
reader: r,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *TimeoutReader) Read(p []byte) (n int, err error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), tr.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
var readErr error
|
||||||
|
var bytesRead int
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
bytesRead, readErr = tr.reader.Read(p)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return bytesRead, readErr
|
||||||
|
case <-ctx.Done():
|
||||||
|
return 0, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeoutReader_Read(t *testing.T) {
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||||
|
|
||||||
|
buf := make([]byte, len(data))
|
||||||
|
n, err := timeoutReader.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if n != len(data) {
|
||||||
|
t.Fatalf("expected to read %d bytes, read %d", len(data), n)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf, data) {
|
||||||
|
t.Fatalf("expected %s, got %s", data, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeoutReader_ReadTimeout(t *testing.T) {
|
||||||
|
reader := &slowReader{delay: 2 * time.Second}
|
||||||
|
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||||
|
|
||||||
|
buf := make([]byte, 8192)
|
||||||
|
_, err := timeoutReader.Read(buf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected timeout error, got nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
t.Fatalf("expected %v, got %v", context.DeadlineExceeded, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type slowReader struct {
|
||||||
|
delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *slowReader) Read(p []byte) (n int, err error) {
|
||||||
|
time.Sleep(sr.delay)
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
+36
-20
@@ -2,12 +2,13 @@ package test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/armon/go-socks5"
|
"github.com/armon/go-socks5"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -24,6 +25,11 @@ const (
|
|||||||
Dir = "./"
|
Dir = "./"
|
||||||
BuildFile = Dir + BuildName
|
BuildFile = Dir + BuildName
|
||||||
|
|
||||||
|
ExternalDownloadUrl = "https://raw.githubusercontent.com/GopeedLab/gopeed/v1.5.6/_docs/img/banner.png"
|
||||||
|
ExternalDownloadName = "banner.png"
|
||||||
|
ExternalDownloadSize = 26416
|
||||||
|
//ExternalDownloadMd5 = "c67c6e3cae79a95342485676571e8a5c"
|
||||||
|
|
||||||
DownloadName = "download.data"
|
DownloadName = "download.data"
|
||||||
DownloadRename = "download (1).data"
|
DownloadRename = "download (1).data"
|
||||||
DownloadFile = Dir + DownloadName
|
DownloadFile = Dir + DownloadName
|
||||||
@@ -31,7 +37,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func StartTestFileServer() net.Listener {
|
func StartTestFileServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
return http.FileServer(http.Dir(Dir))
|
return http.FileServer(http.Dir(Dir))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -47,7 +53,7 @@ func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
return &SlowFileServer{
|
return &SlowFileServer{
|
||||||
delay: delay,
|
delay: delay,
|
||||||
handler: http.FileServer(http.Dir(Dir)),
|
handler: http.FileServer(http.Dir(Dir)),
|
||||||
@@ -56,7 +62,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestCustomServer() net.Listener {
|
func StartTestCustomServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
file, err := os.Open(BuildFile)
|
file, err := os.Open(BuildFile)
|
||||||
@@ -83,7 +89,7 @@ func StartTestCustomServer() net.Listener {
|
|||||||
|
|
||||||
func StartTestRetryServer() net.Listener {
|
func StartTestRetryServer() net.Listener {
|
||||||
counter := 0
|
counter := 0
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
@@ -103,7 +109,7 @@ func StartTestRetryServer() net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestPostServer() net.Listener {
|
func StartTestPostServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
||||||
@@ -127,7 +133,7 @@ func StartTestPostServer() net.Listener {
|
|||||||
|
|
||||||
func StartTestErrorServer() net.Listener {
|
func StartTestErrorServer() net.Listener {
|
||||||
counter := 0
|
counter := 0
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
@@ -144,7 +150,7 @@ func StartTestErrorServer() net.Listener {
|
|||||||
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||||
var connections atomic.Int32
|
var connections atomic.Int32
|
||||||
|
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -160,12 +166,14 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
if r == "" {
|
if r == "" {
|
||||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
writer.WriteHeader(200)
|
writer.WriteHeader(200)
|
||||||
|
(writer.(http.Flusher)).Flush()
|
||||||
|
|
||||||
file, err := os.Open(BuildFile)
|
file, err := os.Open(BuildFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
slowCopy(writer, file, delay)
|
slowCopy(sl, writer, file, delay)
|
||||||
} else {
|
} else {
|
||||||
// split range
|
// split range
|
||||||
s := strings.Split(r, "=")
|
s := strings.Split(r, "=")
|
||||||
@@ -199,6 +207,8 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||||
writer.Header().Set("Accept-Ranges", "bytes")
|
writer.Header().Set("Accept-Ranges", "bytes")
|
||||||
writer.WriteHeader(206)
|
writer.WriteHeader(206)
|
||||||
|
(writer.(http.Flusher)).Flush()
|
||||||
|
|
||||||
file, err := os.Open(BuildFile)
|
file, err := os.Open(BuildFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writer.WriteHeader(500)
|
writer.WriteHeader(500)
|
||||||
@@ -206,7 +216,7 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
file.Seek(start, 0)
|
file.Seek(start, 0)
|
||||||
slowCopyN(writer, file, end-start+1, delay)
|
slowCopyN(sl, writer, file, end-start+1, delay)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return mux
|
return mux
|
||||||
@@ -214,9 +224,12 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
|
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
|
||||||
func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
||||||
buf := make([]byte, 32*1024)
|
buf := make([]byte, 32*1024)
|
||||||
for {
|
for {
|
||||||
|
if sl.isShutdown {
|
||||||
|
return 0, errors.New("server shutdown")
|
||||||
|
}
|
||||||
nr, er := src.Read(buf)
|
nr, er := src.Read(buf)
|
||||||
if nr > 0 {
|
if nr > 0 {
|
||||||
nw, ew := dst.Write(buf[0:nr])
|
nw, ew := dst.Write(buf[0:nr])
|
||||||
@@ -245,8 +258,8 @@ func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err err
|
|||||||
return written, err
|
return written, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
func slowCopyN(sl *shutdownListener, dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||||
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
|
written, err = slowCopy(sl, dst, io.LimitReader(src, n), delay)
|
||||||
if written == n {
|
if written == n {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
@@ -257,7 +270,7 @@ func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int6
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.Listener {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -267,7 +280,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
// 随机生成一个文件
|
// Write random data
|
||||||
l := int64(8192)
|
l := int64(8192)
|
||||||
buf := make([]byte, l)
|
buf := make([]byte, l)
|
||||||
size := int64(0)
|
size := int64(0)
|
||||||
@@ -284,21 +297,24 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
size += l
|
size += l
|
||||||
}
|
}
|
||||||
server := &http.Server{}
|
server := &http.Server{}
|
||||||
server.Handler = serverHandle()
|
sl := &shutdownListener{
|
||||||
go server.Serve(listener)
|
|
||||||
|
|
||||||
return &shutdownListener{
|
|
||||||
server: server,
|
server: server,
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
}
|
}
|
||||||
|
server.Handler = serverHandle(sl)
|
||||||
|
go server.Serve(listener)
|
||||||
|
|
||||||
|
return sl
|
||||||
}
|
}
|
||||||
|
|
||||||
type shutdownListener struct {
|
type shutdownListener struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
|
isShutdown bool
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *shutdownListener) Close() error {
|
func (c *shutdownListener) Close() error {
|
||||||
|
c.isShutdown = true
|
||||||
closeErr := c.server.Shutdown(context.Background())
|
closeErr := c.server.Shutdown(context.Background())
|
||||||
if err := ifExistAndRemove(BuildFile); err != nil {
|
if err := ifExistAndRemove(BuildFile); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
NotFound = errors.New("not found")
|
|
||||||
BadParams = errors.New("bad params")
|
|
||||||
)
|
|
||||||
+159
-6
@@ -3,7 +3,11 @@ package base
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/mattn/go-ieproxy"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +17,10 @@ type Request struct {
|
|||||||
Extra any `json:"extra"`
|
Extra any `json:"extra"`
|
||||||
// Labels is used to mark the download task
|
// Labels is used to mark the download task
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
|
// Proxy is special proxy config for request
|
||||||
|
Proxy *RequestProxy `json:"proxy"`
|
||||||
|
// SkipVerifyCert is the flag that skip verify cert
|
||||||
|
SkipVerifyCert bool `json:"skipVerifyCert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Validate() error {
|
func (r *Request) Validate() error {
|
||||||
@@ -22,6 +30,37 @@ func (r *Request) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestProxyMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RequestProxyModeFollow follow setting proxy
|
||||||
|
RequestProxyModeFollow RequestProxyMode = "follow"
|
||||||
|
// RequestProxyModeNone not use proxy
|
||||||
|
RequestProxyModeNone RequestProxyMode = "none"
|
||||||
|
// RequestProxyModeCustom custom proxy
|
||||||
|
RequestProxyModeCustom RequestProxyMode = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestProxy struct {
|
||||||
|
Mode RequestProxyMode `json:"mode"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Usr string `json:"usr"`
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||||
|
if p == nil || p.Mode != RequestProxyModeCustom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Scheme == "" || p.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd))
|
||||||
|
}
|
||||||
|
|
||||||
// Resource download resource
|
// Resource download resource
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
// if name is not empty, the resource is a folder and the name is the folder name
|
// if name is not empty, the resource is a folder and the name is the folder name
|
||||||
@@ -91,12 +130,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) Clone() *Options {
|
func (o *Options) Clone() *Options {
|
||||||
return &Options{
|
return util.DeepClone(o)
|
||||||
Name: o.Name,
|
|
||||||
Path: o.Path,
|
|
||||||
SelectFiles: o.SelectFiles,
|
|
||||||
Extra: o.Extra,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseReqExtra[E any](req *Request) error {
|
func ParseReqExtra[E any](req *Request) error {
|
||||||
@@ -128,3 +162,122 @@ func ParseOptsExtra[E any](opts *Options) error {
|
|||||||
opts.Extra = &t
|
opts.Extra = &t
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||||
|
type DownloaderStoreConfig struct {
|
||||||
|
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||||
|
|
||||||
|
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||||
|
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||||
|
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||||
|
Extra map[string]any `json:"extra"`
|
||||||
|
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||||
|
if cfg.MaxRunning == 0 {
|
||||||
|
cfg.MaxRunning = 5
|
||||||
|
}
|
||||||
|
if cfg.ProtocolConfig == nil {
|
||||||
|
cfg.ProtocolConfig = make(map[string]any)
|
||||||
|
}
|
||||||
|
if cfg.Proxy == nil {
|
||||||
|
cfg.Proxy = &DownloaderProxyConfig{}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *DownloaderStoreConfig {
|
||||||
|
if beforeCfg == nil {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
if cfg.DownloadDir == "" {
|
||||||
|
cfg.DownloadDir = beforeCfg.DownloadDir
|
||||||
|
}
|
||||||
|
if cfg.MaxRunning == 0 {
|
||||||
|
cfg.MaxRunning = beforeCfg.MaxRunning
|
||||||
|
}
|
||||||
|
if cfg.ProtocolConfig == nil {
|
||||||
|
cfg.ProtocolConfig = beforeCfg.ProtocolConfig
|
||||||
|
}
|
||||||
|
if cfg.Extra == nil {
|
||||||
|
cfg.Extra = beforeCfg.Extra
|
||||||
|
}
|
||||||
|
if cfg.Proxy == nil {
|
||||||
|
cfg.Proxy = beforeCfg.Proxy
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloaderProxyConfig struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
// System is the flag that use system proxy
|
||||||
|
System bool `json:"system"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Usr string `json:"usr"`
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||||
|
if cfg == nil || cfg.Enable == false {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.System {
|
||||||
|
safeProxyReloadConf()
|
||||||
|
return ieproxy.GetProxyFunc()
|
||||||
|
}
|
||||||
|
if cfg.Scheme == "" || cfg.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return http.ProxyURL(util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToUrl returns the proxy url, just for git clone
|
||||||
|
func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
||||||
|
if cfg == nil || cfg.Enable == false {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.System {
|
||||||
|
safeProxyReloadConf()
|
||||||
|
static := ieproxy.GetConf().Static
|
||||||
|
if static.Active && len(static.Protocols) > 0 {
|
||||||
|
// If only one protocol, use it
|
||||||
|
if len(static.Protocols) == 1 {
|
||||||
|
for _, v := range static.Protocols {
|
||||||
|
return parseUrlSafe(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check https
|
||||||
|
if v, ok := static.Protocols["https"]; ok {
|
||||||
|
return parseUrlSafe(v)
|
||||||
|
}
|
||||||
|
// Check http
|
||||||
|
if v, ok := static.Protocols["http"]; ok {
|
||||||
|
return parseUrlSafe(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.Scheme == "" || cfg.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prcLock sync.Mutex
|
||||||
|
|
||||||
|
func safeProxyReloadConf() {
|
||||||
|
prcLock.Lock()
|
||||||
|
defer prcLock.Unlock()
|
||||||
|
|
||||||
|
ieproxy.ReloadConf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUrlSafe(rawUrl string) *url.URL {
|
||||||
|
u, err := url.Parse(rawUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields *DownloaderStoreConfig
|
||||||
|
want *DownloaderStoreConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Init",
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 5,
|
||||||
|
ProtocolConfig: map[string]any{},
|
||||||
|
Proxy: &DownloaderProxyConfig{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Init MaxRunning",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 10,
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 10,
|
||||||
|
ProtocolConfig: map[string]any{},
|
||||||
|
Proxy: &DownloaderProxyConfig{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Init ProtocolConfig",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
ProtocolConfig: map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 5,
|
||||||
|
ProtocolConfig: map[string]any{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
Proxy: &DownloaderProxyConfig{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Init Proxy",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
Proxy: &DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 5,
|
||||||
|
ProtocolConfig: map[string]any{},
|
||||||
|
Proxy: &DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := &DownloaderStoreConfig{
|
||||||
|
FirstLoad: tt.fields.FirstLoad,
|
||||||
|
DownloadDir: tt.fields.DownloadDir,
|
||||||
|
MaxRunning: tt.fields.MaxRunning,
|
||||||
|
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||||
|
Extra: tt.fields.Extra,
|
||||||
|
Proxy: tt.fields.Proxy,
|
||||||
|
}
|
||||||
|
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Init() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloaderStoreConfig_Merge(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
beforeCfg *DownloaderStoreConfig
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields *DownloaderStoreConfig
|
||||||
|
args args
|
||||||
|
want *DownloaderStoreConfig
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Merge Nil",
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
args{
|
||||||
|
beforeCfg: nil,
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge DownloadDir No Override",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
DownloadDir: "before",
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
DownloadDir: "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
DownloadDir: "before",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge DownloadDir Override",
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
DownloadDir: "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
DownloadDir: "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge MaxRunning No Override",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 1,
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
MaxRunning: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge MaxRunning Override",
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
MaxRunning: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
MaxRunning: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge ProtocolConfig No Override",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
ProtocolConfig: map[string]any{},
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
ProtocolConfig: map[string]any{
|
||||||
|
"key": "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
ProtocolConfig: map[string]any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge ProtocolConfig Override",
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
ProtocolConfig: map[string]any{
|
||||||
|
"key": "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
ProtocolConfig: map[string]any{
|
||||||
|
"key": "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge Extra No Override",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
Extra: map[string]any{},
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
Extra: map[string]any{
|
||||||
|
"key": "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
Extra: map[string]any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge Extra Override",
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
Extra: map[string]any{
|
||||||
|
"key": "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
Extra: map[string]any{
|
||||||
|
"key": "after",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge Proxy No Override",
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
Proxy: &DownloaderProxyConfig{},
|
||||||
|
},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
Proxy: &DownloaderProxyConfig{
|
||||||
|
Scheme: "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
Proxy: &DownloaderProxyConfig{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Merge Proxy Override",
|
||||||
|
&DownloaderStoreConfig{},
|
||||||
|
args{
|
||||||
|
beforeCfg: &DownloaderStoreConfig{
|
||||||
|
Proxy: &DownloaderProxyConfig{
|
||||||
|
Scheme: "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&DownloaderStoreConfig{
|
||||||
|
Proxy: &DownloaderProxyConfig{
|
||||||
|
Scheme: "http",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := &DownloaderStoreConfig{
|
||||||
|
FirstLoad: tt.fields.FirstLoad,
|
||||||
|
DownloadDir: tt.fields.DownloadDir,
|
||||||
|
MaxRunning: tt.fields.MaxRunning,
|
||||||
|
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||||
|
Extra: tt.fields.Extra,
|
||||||
|
Proxy: tt.fields.Proxy,
|
||||||
|
}
|
||||||
|
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Merge() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+427
-208
@@ -11,9 +11,12 @@ import (
|
|||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/pkgerrors"
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
"github.com/virtuald/go-paniclog"
|
"math"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -42,25 +45,29 @@ var (
|
|||||||
type Listener func(event *Event)
|
type Listener func(event *Event)
|
||||||
|
|
||||||
type Progress struct {
|
type Progress struct {
|
||||||
// 下载耗时(纳秒)
|
// Total download time(ns)
|
||||||
Used int64 `json:"used"`
|
Used int64 `json:"used"`
|
||||||
// 每秒下载字节数
|
// Download speed(bytes/s)
|
||||||
Speed int64 `json:"speed"`
|
Speed int64 `json:"speed"`
|
||||||
// 已下载的字节数
|
// Downloaded size(bytes)
|
||||||
Downloaded int64 `json:"downloaded"`
|
Downloaded int64 `json:"downloaded"`
|
||||||
|
// Uploaded speed(bytes/s)
|
||||||
|
UploadSpeed int64 `json:"uploadSpeed"`
|
||||||
|
// Uploaded size(bytes)
|
||||||
|
Uploaded int64 `json:"uploaded"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Downloader struct {
|
type Downloader struct {
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
ExtensionLogger *logger.Logger
|
ExtensionLogger *logger.Logger
|
||||||
|
|
||||||
cfg *DownloaderConfig
|
cfg *DownloaderConfig
|
||||||
fetcherBuilders map[string]fetcher.FetcherBuilder
|
fetcherCache map[string]fetcher.Fetcher
|
||||||
fetcherCache map[string]fetcher.Fetcher
|
storage Storage
|
||||||
storage Storage
|
tasks []*Task
|
||||||
tasks []*Task
|
waitTasks []*Task
|
||||||
waitTasks []*Task
|
watchedTasks sync.Map
|
||||||
listener Listener
|
listener Listener
|
||||||
|
|
||||||
lock *sync.Mutex
|
lock *sync.Mutex
|
||||||
fetcherMapLock *sync.RWMutex
|
fetcherMapLock *sync.RWMutex
|
||||||
@@ -77,11 +84,10 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
|||||||
cfg.Init()
|
cfg.Init()
|
||||||
|
|
||||||
d := &Downloader{
|
d := &Downloader{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
fetcherBuilders: make(map[string]fetcher.FetcherBuilder),
|
fetcherCache: make(map[string]fetcher.Fetcher),
|
||||||
fetcherCache: make(map[string]fetcher.Fetcher),
|
waitTasks: make([]*Task, 0),
|
||||||
waitTasks: make([]*Task, 0),
|
storage: cfg.Storage,
|
||||||
storage: cfg.Storage,
|
|
||||||
|
|
||||||
lock: &sync.Mutex{},
|
lock: &sync.Mutex{},
|
||||||
fetcherMapLock: &sync.RWMutex{},
|
fetcherMapLock: &sync.RWMutex{},
|
||||||
@@ -89,11 +95,6 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
|||||||
|
|
||||||
extensions: make([]*Extension, 0),
|
extensions: make([]*Extension, 0),
|
||||||
}
|
}
|
||||||
for _, f := range cfg.FetchBuilders {
|
|
||||||
for _, p := range f.Schemes() {
|
|
||||||
d.fetcherBuilders[strings.ToUpper(p)] = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||||
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
|
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
|
||||||
@@ -110,7 +111,7 @@ func (d *Downloader) Setup() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// load config from storage
|
// load config from storage
|
||||||
var cfg DownloaderStoreConfig
|
var cfg base.DownloaderStoreConfig
|
||||||
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
|
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -118,17 +119,20 @@ func (d *Downloader) Setup() error {
|
|||||||
if exist {
|
if exist {
|
||||||
d.cfg.DownloaderStoreConfig = &cfg
|
d.cfg.DownloaderStoreConfig = &cfg
|
||||||
} else {
|
} else {
|
||||||
d.cfg.DownloaderStoreConfig = &DownloaderStoreConfig{
|
d.cfg.DownloaderStoreConfig = &base.DownloaderStoreConfig{
|
||||||
FirstLoad: true,
|
FirstLoad: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// init default config
|
// init default config
|
||||||
d.cfg.DownloaderStoreConfig.Init()
|
d.cfg.DownloaderStoreConfig.Init()
|
||||||
for _, fb := range d.fetcherBuilders {
|
// init protocol config, if not exist, use default config
|
||||||
f := fb.Build()
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
d.setupFetcher(f)
|
protocol := fm.Name()
|
||||||
|
if _, ok := d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol]; !ok {
|
||||||
|
d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol] = fm.DefaultConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load tasks from storage
|
// load tasks from storage
|
||||||
var tasks []*Task
|
var tasks []*Task
|
||||||
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
||||||
@@ -166,6 +170,22 @@ func (d *Downloader) Setup() error {
|
|||||||
}
|
}
|
||||||
d.extensions = extensions
|
d.extensions = extensions
|
||||||
|
|
||||||
|
// handle upload
|
||||||
|
go func() {
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
if task.Status == base.DownloadStatusDone && task.Uploading {
|
||||||
|
if err := d.restoreTask(task); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("task upload restore fetcher failed, task id: %s", task.ID)
|
||||||
|
}
|
||||||
|
if uploader, ok := task.fetcher.(fetcher.Uploader); ok {
|
||||||
|
if err := uploader.Upload(); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("task upload failed, task id: %s", task.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// calculate download speed every tick
|
// calculate download speed every tick
|
||||||
go func() {
|
go func() {
|
||||||
for !d.closed.Load() {
|
for !d.closed.Load() {
|
||||||
@@ -174,34 +194,39 @@ func (d *Downloader) Setup() error {
|
|||||||
func() {
|
func() {
|
||||||
task.statusLock.Lock()
|
task.statusLock.Lock()
|
||||||
defer task.statusLock.Unlock()
|
defer task.statusLock.Unlock()
|
||||||
if task.Status != base.DownloadStatusRunning {
|
if task.Status != base.DownloadStatusRunning && !task.Uploading {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check if task is deleted
|
// check if task is deleted
|
||||||
if d.GetTask(task.ID) == nil {
|
if d.GetTask(task.ID) == nil || task.fetcher == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
current := task.fetcher.Progress().TotalDownloaded()
|
current := task.fetcher.Progress().TotalDownloaded()
|
||||||
task.Progress.Used = task.timer.Used()
|
tick := float64(d.cfg.RefreshInterval) / 1000
|
||||||
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000)
|
downloadDataChanged := false
|
||||||
task.Progress.Downloaded = current
|
if task.Status == base.DownloadStatusRunning {
|
||||||
|
downloadDataChanged = current != task.Progress.Downloaded
|
||||||
|
task.Progress.Used = task.timer.Used()
|
||||||
|
task.Progress.Speed = task.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick)
|
||||||
|
task.Progress.Downloaded = current
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadDataChanged := false
|
||||||
|
if task.Uploading {
|
||||||
|
uploader := task.fetcher.(fetcher.Uploader)
|
||||||
|
currentUploaded := uploader.UploadedBytes()
|
||||||
|
uploadDataChanged = currentUploaded != task.Progress.Uploaded
|
||||||
|
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
|
||||||
|
task.Progress.Uploaded = currentUploaded
|
||||||
|
}
|
||||||
d.emit(EventKeyProgress, task)
|
d.emit(EventKeyProgress, task)
|
||||||
|
|
||||||
// store fetcher progress
|
// store fetcher progress when download/upload data changed
|
||||||
data, err := task.fetcherBuilder.Store(task.fetcher)
|
if !downloadDataChanged && !uploadDataChanged {
|
||||||
if err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := d.storage.Put(bucketSave, task.ID, data); err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := d.storage.Put(bucketTask, task.ID, task); err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
d.saveTask(task)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,24 +236,56 @@ func (d *Downloader) Setup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
|
func (d *Downloader) parseFm(url string) (fetcher.FetcherManager, error) {
|
||||||
schema := util.ParseSchema(url)
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
fetchBuilder, ok := d.fetcherBuilders[schema]
|
for _, filter := range fm.Filters() {
|
||||||
if ok {
|
if filter.Match(url) {
|
||||||
return fetchBuilder, nil
|
return fm, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, ErrUnSupportedProtocol
|
return nil, ErrUnSupportedProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fetcher) {
|
||||||
ctl := controller.NewController()
|
ctl := controller.NewController()
|
||||||
ctl.GetConfig = func(v any) bool {
|
ctl.GetConfig = func(v any) {
|
||||||
return d.getProtocolConfig(fetcher.Name(), v)
|
d.getProtocolConfig(fm.Name(), v)
|
||||||
|
}
|
||||||
|
// Get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||||
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
if requestProxy == nil {
|
||||||
|
return d.cfg.Proxy.ToHandler()
|
||||||
|
}
|
||||||
|
switch requestProxy.Mode {
|
||||||
|
case base.RequestProxyModeNone:
|
||||||
|
return nil
|
||||||
|
case base.RequestProxyModeCustom:
|
||||||
|
return requestProxy.ToHandler()
|
||||||
|
default:
|
||||||
|
return d.cfg.Proxy.ToHandler()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctl.ProxyUrl = d.cfg.ProxyUrl()
|
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) saveTask(task *Task) error {
|
||||||
|
data, err := task.fetcherManager.Store(task.fetcher)
|
||||||
|
if err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.storage.Put(bucketSave, task.ID, data); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.storage.Put(bucketTask, task.ID, task); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
|
func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
|
||||||
rrId, err := gonanoid.New()
|
rrId, err := gonanoid.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -301,6 +358,18 @@ 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) {
|
||||||
|
taskIds := make([]string, 0)
|
||||||
|
for _, req := range reqs {
|
||||||
|
taskId, err := d.CreateDirect(req, opts.Clone())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
taskIds = append(taskIds, taskId)
|
||||||
|
}
|
||||||
|
return taskIds, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
|
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
|
||||||
d.fetcherMapLock.RLock()
|
d.fetcherMapLock.RLock()
|
||||||
fetcher, ok := d.fetcherCache[rrId]
|
fetcher, ok := d.fetcherCache[rrId]
|
||||||
@@ -316,56 +385,28 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
|
|||||||
return d.doCreate(fetcher, opts)
|
return d.doCreate(fetcher, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Pause(id string) (err error) {
|
func (d *Downloader) Pause(filter *TaskFilter) (err error) {
|
||||||
task := d.GetTask(id)
|
if filter == nil || filter.IsEmpty() {
|
||||||
if task == nil {
|
return d.pauseAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.NotStatuses = []base.Status{base.DownloadStatusPause, base.DownloadStatusError, base.DownloadStatusDone}
|
||||||
|
pauseTasks := d.GetTasksByFilter(filter)
|
||||||
|
if len(pauseTasks) == 0 {
|
||||||
return ErrTaskNotFound
|
return ErrTaskNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func() {
|
for _, task := range pauseTasks {
|
||||||
task.lock.Lock()
|
|
||||||
defer task.lock.Unlock()
|
|
||||||
|
|
||||||
if task.Status == base.DownloadStatusPause {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = d.doPause(task); err != nil {
|
if err = d.doPause(task); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.notifyRunning()
|
}
|
||||||
}()
|
d.notifyRunning()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Continue(id string) (err error) {
|
func (d *Downloader) pauseAll() (err error) {
|
||||||
task := d.GetTask(id)
|
|
||||||
if task == nil {
|
|
||||||
return ErrTaskNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func() {
|
|
||||||
d.lock.Lock()
|
|
||||||
defer d.lock.Unlock()
|
|
||||||
|
|
||||||
remainRunningCount := d.remainRunningCount()
|
|
||||||
if remainRunningCount == 0 {
|
|
||||||
for _, t := range d.tasks {
|
|
||||||
if t.Status == base.DownloadStatusRunning {
|
|
||||||
if err = d.doPause(t); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Status = base.DownloadStatusWait
|
|
||||||
d.waitTasks = append(d.waitTasks, t)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return d.doStart(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Downloader) PauseAll() (err error) {
|
|
||||||
func() {
|
func() {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
@@ -383,7 +424,65 @@ func (d *Downloader) PauseAll() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) ContinueAll() (err error) {
|
// Continue specific tasks, if continue tasks will exceed maxRunning, it needs pause some running tasks before that
|
||||||
|
func (d *Downloader) Continue(filter *TaskFilter) (err error) {
|
||||||
|
if filter == nil || filter.IsEmpty() {
|
||||||
|
return d.continueAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.NotStatuses = []base.Status{base.DownloadStatusRunning, base.DownloadStatusDone}
|
||||||
|
continueTasks := d.GetTasksByFilter(filter)
|
||||||
|
if len(continueTasks) == 0 {
|
||||||
|
return ErrTaskNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
realContinueTasks := make([]*Task, 0)
|
||||||
|
func() {
|
||||||
|
d.lock.Lock()
|
||||||
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
continueCount := len(continueTasks)
|
||||||
|
remainRunningCount := d.remainRunningCount()
|
||||||
|
needRunningCount := int(math.Min(float64(d.cfg.MaxRunning), float64(continueCount)))
|
||||||
|
needPauseCount := needRunningCount - remainRunningCount
|
||||||
|
if needPauseCount > 0 {
|
||||||
|
pausedCount := 0
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
if task.Status == base.DownloadStatusRunning {
|
||||||
|
if err = d.doPause(task); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.Status = base.DownloadStatusWait
|
||||||
|
d.waitTasks = append(d.waitTasks, task)
|
||||||
|
pausedCount++
|
||||||
|
}
|
||||||
|
if pausedCount == needPauseCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, task := range continueTasks {
|
||||||
|
if len(realContinueTasks) < needRunningCount {
|
||||||
|
realContinueTasks = append(realContinueTasks, task)
|
||||||
|
} else {
|
||||||
|
task.Status = base.DownloadStatusWait
|
||||||
|
d.waitTasks = append(d.waitTasks, task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, task := range realContinueTasks {
|
||||||
|
if err = d.doStart(task); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// continueAll continue all tasks but does not affect tasks already running
|
||||||
|
func (d *Downloader) continueAll() (err error) {
|
||||||
continuedTasks := make([]*Task, 0)
|
continuedTasks := make([]*Task, 0)
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
@@ -404,8 +503,7 @@ func (d *Downloader) ContinueAll() (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
for _, task := range continuedTasks {
|
for _, task := range continuedTasks {
|
||||||
tt := task
|
if err = d.doStart(task); err != nil {
|
||||||
if err = d.doStart(tt); err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,47 +511,35 @@ func (d *Downloader) ContinueAll() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Delete(id string, force bool) (err error) {
|
func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
|
||||||
task := d.GetTask(id)
|
if filter == nil || filter.IsEmpty() {
|
||||||
if task == nil {
|
return d.continueAll()
|
||||||
return ErrTaskNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func() {
|
continueTasks := d.GetTasksByFilter(filter)
|
||||||
d.lock.Lock()
|
for _, task := range continueTasks {
|
||||||
defer d.lock.Unlock()
|
if err = d.doStart(task); err != nil {
|
||||||
|
return
|
||||||
for i, t := range d.tasks {
|
|
||||||
if t.ID == id {
|
|
||||||
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for i, t := range d.waitTasks {
|
|
||||||
if t.ID == id {
|
|
||||||
d.waitTasks = append(d.waitTasks[:i], d.waitTasks[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = d.doDelete(task, force)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
d.notifyRunning()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err error) {
|
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||||
deleteTasks := d.GetTasksByStatues(statues)
|
if filter == nil || filter.IsEmpty() {
|
||||||
|
return d.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTasks := d.GetTasksByFilter(filter)
|
||||||
if len(deleteTasks) == 0 {
|
if len(deleteTasks) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteIds := make([]string, 0)
|
deleteIds := make([]string, 0)
|
||||||
|
deleteTasksPtr := make([]*Task, 0)
|
||||||
for _, task := range deleteTasks {
|
for _, task := range deleteTasks {
|
||||||
deleteIds = append(deleteIds, task.ID)
|
deleteIds = append(deleteIds, task.ID)
|
||||||
|
deleteTasksPtr = append(deleteTasksPtr, task)
|
||||||
}
|
}
|
||||||
func() {
|
func() {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
@@ -475,7 +561,7 @@ func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err err
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, task := range deleteTasks {
|
for _, task := range deleteTasksPtr {
|
||||||
err = d.doDelete(task, force)
|
err = d.doDelete(task, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -486,20 +572,45 @@ func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) deleteAll() (err error) {
|
||||||
|
func() {
|
||||||
|
d.lock.Lock()
|
||||||
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
d.tasks = make([]*Task, 0)
|
||||||
|
d.waitTasks = make([]*Task, 0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
if err = d.doDelete(task, true); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) Stats(id string) (sr any, err error) {
|
func (d *Downloader) Stats(id string) (sr any, err error) {
|
||||||
task := d.GetTask(id)
|
task := d.GetTask(id)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return sr, ErrTaskNotFound
|
return sr, ErrTaskNotFound
|
||||||
}
|
}
|
||||||
|
if task.fetcher == nil {
|
||||||
|
err = func() error {
|
||||||
|
task.statusLock.Lock()
|
||||||
|
defer task.statusLock.Unlock()
|
||||||
|
|
||||||
|
return d.restoreFetcher(task)
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
sr = task.fetcher.Stats()
|
sr = task.fetcher.Stats()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
||||||
err = func() error {
|
err = func() error {
|
||||||
d.lock.Lock()
|
|
||||||
defer d.lock.Unlock()
|
|
||||||
|
|
||||||
if err := d.storage.Delete(bucketTask, task.ID); err != nil {
|
if err := d.storage.Delete(bucketTask, task.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -527,19 +638,32 @@ func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
|||||||
task = nil
|
task = nil
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("delete task failed, task id: %s", task.ID)
|
d.Logger.Error().Stack().Err(err).Msgf("delete task failed, task id: %s", task.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Close() error {
|
func (d *Downloader) Close() error {
|
||||||
d.closed.Store(true)
|
d.closed.Store(true)
|
||||||
if err := d.PauseAll(); err != nil {
|
|
||||||
return err
|
closeArr := []func() error{
|
||||||
|
d.pauseAll,
|
||||||
}
|
}
|
||||||
return d.storage.Close()
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
|
closeArr = append(closeArr, fm.Close)
|
||||||
|
}
|
||||||
|
closeArr = append(closeArr, d.storage.Close)
|
||||||
|
// Make sure all resources are released, if had error, return the last error
|
||||||
|
var lastErr error
|
||||||
|
for i, close := range closeArr {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("downloader close failed, index: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Clear() error {
|
func (d *Downloader) Clear() error {
|
||||||
@@ -587,27 +711,61 @@ func (d *Downloader) GetTasks() []*Task {
|
|||||||
return d.tasks
|
return d.tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) GetTasksByStatues(statues []base.Status) []*Task {
|
// GetTasksByFilter get tasks by filter, if filter is nil, return all tasks
|
||||||
if len(statues) == 0 {
|
// return tasks and if match all tasks
|
||||||
|
func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||||
|
if filter == nil || filter.IsEmpty() {
|
||||||
return d.tasks
|
return d.tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
idMatch := func(task *Task) bool {
|
||||||
|
if len(filter.IDs) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, id := range filter.IDs {
|
||||||
|
if task.ID == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
statusMatch := func(task *Task) bool {
|
||||||
|
if len(filter.Statuses) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, status := range filter.Statuses {
|
||||||
|
if task.Status == status {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
notStatusMatch := func(task *Task) bool {
|
||||||
|
if len(filter.NotStatuses) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, status := range filter.NotStatuses {
|
||||||
|
if task.Status == status {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
tasks := make([]*Task, 0)
|
tasks := make([]*Task, 0)
|
||||||
for _, task := range d.tasks {
|
for _, task := range d.tasks {
|
||||||
for _, status := range statues {
|
if idMatch(task) && statusMatch(task) && notStatusMatch(task) {
|
||||||
if task.Status == status {
|
tasks = append(tasks, task)
|
||||||
tasks = append(tasks, task)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tasks
|
return tasks
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
|
func (d *Downloader) GetConfig() (*base.DownloaderStoreConfig, error) {
|
||||||
return d.cfg.DownloaderStoreConfig, nil
|
return d.cfg.DownloaderStoreConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
|
func (d *Downloader) PutConfig(v *base.DownloaderStoreConfig) error {
|
||||||
d.cfg.DownloaderStoreConfig = v
|
d.cfg.DownloaderStoreConfig = v
|
||||||
return d.storage.Put(bucketConfig, "config", v)
|
return d.storage.Put(bucketConfig, "config", v)
|
||||||
}
|
}
|
||||||
@@ -629,11 +787,49 @@ func (d *Downloader) getProtocolConfig(name string, v any) bool {
|
|||||||
|
|
||||||
// wait task done
|
// wait task done
|
||||||
func (d *Downloader) watch(task *Task) {
|
func (d *Downloader) watch(task *Task) {
|
||||||
|
if _, loaded := d.watchedTasks.LoadOrStore(task.ID, true); loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
d.watchedTasks.Delete(task.ID)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait task upload done
|
||||||
|
if task.Uploading {
|
||||||
|
if uploader, ok := task.fetcher.(fetcher.Uploader); ok {
|
||||||
|
go func() {
|
||||||
|
err := uploader.WaitUpload()
|
||||||
|
if err != nil {
|
||||||
|
d.Logger.Warn().Err(err).Msgf("task wait upload failed, task id: %s", task.ID)
|
||||||
|
}
|
||||||
|
d.lock.Lock()
|
||||||
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
// Check if the task is deleted
|
||||||
|
if d.GetTask(task.ID) != nil {
|
||||||
|
task.Uploading = false
|
||||||
|
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.Status == base.DownloadStatusDone {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := task.fetcher.Wait()
|
err := task.fetcher.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.doOnError(task, err)
|
d.doOnError(task, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When delete a not resolved task, need check if the task resource is nil
|
||||||
|
if task.Meta.Res == nil || d.GetTask(task.ID) == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
task.Progress.Used = task.timer.Used()
|
task.Progress.Used = task.timer.Used()
|
||||||
if task.Meta.Res.Size == 0 {
|
if task.Meta.Res.Size == 0 {
|
||||||
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
||||||
@@ -650,6 +846,7 @@ func (d *Downloader) watch(task *Task) {
|
|||||||
d.emit(EventKeyDone, task)
|
d.emit(EventKeyDone, task)
|
||||||
d.emit(EventKeyFinally, task, err)
|
d.emit(EventKeyFinally, task, err)
|
||||||
d.notifyRunning()
|
d.notifyRunning()
|
||||||
|
d.triggerOnDone(task)
|
||||||
|
|
||||||
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
||||||
downloadFilePath := task.Meta.SingleFilepath()
|
downloadFilePath := task.Meta.SingleFilepath()
|
||||||
@@ -683,46 +880,51 @@ func (d *Downloader) doOnError(task *Task, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) restoreFetcher(task *Task) error {
|
func (d *Downloader) restoreTask(task *Task) error {
|
||||||
if task.fetcher == nil {
|
if task.fetcher == nil {
|
||||||
fb, err := d.parseFb(task.Meta.Req.URL)
|
if err := d.restoreFetcher(task); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task.fetcherBuilder = fb
|
|
||||||
err = func() error {
|
|
||||||
v, f := fb.Restore()
|
|
||||||
if v != nil {
|
|
||||||
err := d.storage.Pop(bucketSave, task.ID, v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
task.fetcher = f(task.Meta, v)
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("deserialize fetcher failed, task id: %s", task.ID)
|
|
||||||
}
|
|
||||||
if task.fetcher == nil {
|
|
||||||
task.fetcher = fb.Build()
|
|
||||||
}
|
|
||||||
d.setupFetcher(task.fetcher)
|
|
||||||
if task.fetcher.Meta().Req == nil {
|
|
||||||
task.fetcher.Meta().Req = task.Meta.Req
|
|
||||||
}
|
|
||||||
if task.fetcher.Meta().Res == nil {
|
|
||||||
task.fetcher.Meta().Res = task.Meta.Res
|
|
||||||
}
|
|
||||||
go d.watch(task)
|
|
||||||
} else if task.Status == base.DownloadStatusError {
|
|
||||||
go d.watch(task)
|
|
||||||
}
|
}
|
||||||
|
go d.watch(task)
|
||||||
task.fetcher.Create(task.Meta.Opts)
|
task.fetcher.Create(task.Meta.Opts)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (taskId string, err error) {
|
func (d *Downloader) restoreFetcher(task *Task) error {
|
||||||
|
var fm fetcher.FetcherManager
|
||||||
|
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 {
|
||||||
|
err := d.storage.Pop(bucketSave, task.ID, v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.fetcher = f(task.Meta, v)
|
||||||
|
if task.fetcher == nil {
|
||||||
|
task.fetcher = task.fetcherManager.Build()
|
||||||
|
}
|
||||||
|
d.setupFetcher(task.fetcherManager, task.fetcher)
|
||||||
|
if task.fetcher.Meta().Req == nil {
|
||||||
|
task.fetcher.Meta().Req = task.Meta.Req
|
||||||
|
}
|
||||||
|
if task.fetcher.Meta().Res == nil {
|
||||||
|
task.fetcher.Meta().Res = task.Meta.Res
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId string, err error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
opts = &base.Options{}
|
opts = &base.Options{}
|
||||||
}
|
}
|
||||||
@@ -730,7 +932,7 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
|||||||
opts.SelectFiles = make([]int, 0)
|
opts.SelectFiles = make([]int, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := fetcher.Meta()
|
meta := f.Meta()
|
||||||
meta.Opts = opts
|
meta.Opts = opts
|
||||||
if opts.Path == "" {
|
if opts.Path == "" {
|
||||||
storeConfig, err := d.GetConfig()
|
storeConfig, err := d.GetConfig()
|
||||||
@@ -740,19 +942,34 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
|||||||
opts.Path = storeConfig.DownloadDir
|
opts.Path = storeConfig.DownloadDir
|
||||||
}
|
}
|
||||||
|
|
||||||
fb, err := d.parseFb(fetcher.Meta().Req.URL)
|
// if enable white download directory, check if the download directory is in the white list
|
||||||
|
if len(d.cfg.DownloadDirWhiteList) > 0 {
|
||||||
|
inWhiteList := false
|
||||||
|
for _, dir := range d.cfg.DownloadDirWhiteList {
|
||||||
|
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
||||||
|
inWhiteList = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inWhiteList {
|
||||||
|
return "", errors.New("download directory is not in white list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fm, err := d.parseFm(f.Meta().Req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
task := NewTask()
|
task := NewTask()
|
||||||
task.fetcherBuilder = fb
|
task.fetcherManager = fm
|
||||||
task.fetcher = fetcher
|
task.fetcher = f
|
||||||
task.Protocol = fetcher.Name()
|
task.Protocol = fm.Name()
|
||||||
task.Meta = fetcher.Meta()
|
task.Meta = f.Meta()
|
||||||
task.Progress = &Progress{}
|
task.Progress = &Progress{}
|
||||||
|
_, task.Uploading = f.(fetcher.Uploader)
|
||||||
initTask(task)
|
initTask(task)
|
||||||
if err = fetcher.Create(opts); err != nil {
|
if err = f.Create(opts); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||||
@@ -795,14 +1012,12 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.restoreFetcher(task)
|
err = d.restoreTask(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreate = task.Status == base.DownloadStatusReady
|
isCreate = task.Status == base.DownloadStatusReady
|
||||||
|
|
||||||
d.triggerOnStart(task)
|
|
||||||
task.updateStatus(base.DownloadStatusRunning)
|
task.updateStatus(base.DownloadStatusRunning)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -819,6 +1034,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
task.lock.Lock()
|
task.lock.Lock()
|
||||||
defer task.lock.Unlock()
|
defer task.lock.Unlock()
|
||||||
|
|
||||||
|
d.triggerOnStart(task)
|
||||||
if task.Meta.Res == nil {
|
if task.Meta.Res == nil {
|
||||||
err := task.fetcher.Resolve(task.Meta.Req)
|
err := task.fetcher.Resolve(task.Meta.Req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -828,26 +1044,28 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isCreate {
|
if isCreate {
|
||||||
d.checkDuplicateLock.Lock()
|
if task.fetcherManager.AutoRename() {
|
||||||
defer d.checkDuplicateLock.Unlock()
|
d.checkDuplicateLock.Lock()
|
||||||
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
defer d.checkDuplicateLock.Unlock()
|
||||||
// check if the download file is duplicated and rename it automatically.
|
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
||||||
if task.Meta.Res.Name != "" {
|
// check if the download file is duplicated and rename it automatically.
|
||||||
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
|
if task.Meta.Res.Name != "" {
|
||||||
fullDirPath := task.Meta.FolderPath()
|
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
|
||||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
fullDirPath := task.Meta.FolderPath()
|
||||||
if err != nil {
|
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.Meta.Opts.Name = newName
|
||||||
|
} else {
|
||||||
|
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
|
||||||
|
fullFilePath := task.Meta.SingleFilepath()
|
||||||
|
newName, err := util.CheckDuplicateAndRename(fullFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.Meta.Opts.Name = newName
|
||||||
}
|
}
|
||||||
task.Meta.Opts.Name = newName
|
|
||||||
} else {
|
|
||||||
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
|
|
||||||
fullFilePath := task.Meta.SingleFilepath()
|
|
||||||
newName, err := util.CheckDuplicateAndRename(fullFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
task.Meta.Opts.Name = newName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
|
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
|
||||||
@@ -855,10 +1073,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
|
|
||||||
task.Progress.Speed = 0
|
task.Progress.Speed = 0
|
||||||
task.timer.Start()
|
task.timer.Start()
|
||||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
if err := task.fetcher.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := task.fetcher.Start(); err != nil {
|
if err := d.saveTask(task); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.emit(EventKeyStart, task)
|
d.emit(EventKeyStart, task)
|
||||||
@@ -924,16 +1142,16 @@ func logPanic(logDir string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
paniclog.RedirectStderr(f)
|
debug.SetCrashOutput(f, debug.CrashOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||||
fb, err := d.parseFb(url)
|
fm, err := d.parseFm(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fetcher := fb.Build()
|
fetcher := fm.Build()
|
||||||
d.setupFetcher(fetcher)
|
d.setupFetcher(fm, fetcher)
|
||||||
return fetcher, nil
|
return fetcher, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,6 +1161,7 @@ func initTask(task *Task) {
|
|||||||
task.statusLock = &sync.Mutex{}
|
task.statusLock = &sync.Mutex{}
|
||||||
task.lock = &sync.Mutex{}
|
task.lock = &sync.Mutex{}
|
||||||
task.speedArr = make([]int64, 0)
|
task.speedArr = make([]int64, 0)
|
||||||
|
task.uploadSpeedArr = make([]int64, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultDownloader = NewDownloader(nil)
|
var defaultDownloader = NewDownloader(nil)
|
||||||
|
|||||||
+341
-27
@@ -4,6 +4,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -79,43 +82,190 @@ func TestDownloader_Create(t *testing.T) {
|
|||||||
want := test.FileMd5(test.BuildFile)
|
want := test.FileMd5(test.BuildFile)
|
||||||
got := test.FileMd5(test.DownloadFile)
|
got := test.FileMd5(test.DownloadFile)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("Download() got = %v, want %v", got, want)
|
t.Errorf("Downloader_Create() got = %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_CreateNotInWhite(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(&DownloaderConfig{
|
||||||
|
DownloadDirWhiteList: []string{"./downloads"},
|
||||||
|
})
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer downloader.Clear()
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
rr, err := downloader.Resolve(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
_, err = downloader.Create(rr.ID, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !strings.Contains(err.Error(), "white") {
|
||||||
|
t.Errorf("TestDownloader_CreateNotInWhite() got = %v, want %v", err.Error(), "not in white list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_CreateDirectBatch(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(nil)
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
downloader.Delete(nil, true)
|
||||||
|
downloader.Clear()
|
||||||
|
}()
|
||||||
|
|
||||||
|
reqs := make([]*base.Request, 0)
|
||||||
|
fileNames := make([]string, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
reqs = append(reqs, req)
|
||||||
|
if i == 0 {
|
||||||
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
|
} else {
|
||||||
|
arr := strings.Split(test.DownloadName, ".")
|
||||||
|
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(reqs))
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
tasks := downloader.GetTasks()
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("CreateDirectBatch() task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
|
||||||
|
nameMatchMap := make(map[string]bool)
|
||||||
|
for _, name := range fileNames {
|
||||||
|
if _, err := os.Stat(test.Dir + "/" + name); os.IsNotExist(err) {
|
||||||
|
t.Errorf("CreateDirectBatch() file not exist: %v", name)
|
||||||
|
}
|
||||||
|
for _, task := range tasks {
|
||||||
|
if name == task.Meta.Opts.Name {
|
||||||
|
nameMatchMap[task.Meta.Opts.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nameMatchMap) != len(reqs) {
|
||||||
|
t.Errorf("CreateDirectBatch() names got = %v, want %v", len(nameMatchMap), len(reqs))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||||
// No proxy
|
// No proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return nil
|
return nil
|
||||||
})
|
}, nil)
|
||||||
// Disable proxy
|
// Disable proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Enable = false
|
proxyCfg.Enable = false
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
|
}, nil)
|
||||||
|
// Enable system proxy but not set proxy environment variable
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
|
proxyCfg.System = true
|
||||||
|
return proxyCfg
|
||||||
|
}, nil)
|
||||||
|
// Enable proxy but error proxy environment variable
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
|
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
||||||
|
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
||||||
|
proxyCfg.System = true
|
||||||
|
return proxyCfg
|
||||||
|
}, func(err error) {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("doTestDownloaderCreateWithProxy() got = nil, want error")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
// Enable system proxy and set proxy environment variable
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
|
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
||||||
|
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
||||||
|
proxyCfg.System = true
|
||||||
|
return proxyCfg
|
||||||
|
}, nil)
|
||||||
// Invalid proxy scheme
|
// Invalid proxy scheme
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Scheme = ""
|
proxyCfg.Scheme = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, nil)
|
||||||
// Invalid proxy host
|
// Invalid proxy host
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Host = ""
|
proxyCfg.Host = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, nil)
|
||||||
// Use proxy without auth
|
// Use proxy without auth
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, nil)
|
||||||
// Use proxy with auth
|
// Use proxy with auth
|
||||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, nil)
|
||||||
|
|
||||||
|
// Request proxy mode follow
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeFollow
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode none
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeNone
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode custom
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig) {
|
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
||||||
httpListener := test.StartTestFileServer()
|
|
||||||
defer httpListener.Close()
|
|
||||||
usr, pwd := "", ""
|
usr, pwd := "", ""
|
||||||
if auth {
|
if auth {
|
||||||
usr, pwd = "admin", "123"
|
usr, pwd = "admin", "123"
|
||||||
@@ -128,29 +278,45 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer downloader.Clear()
|
defer downloader.Clear()
|
||||||
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&DownloaderProxyConfig{
|
globalProxyCfg := &base.DownloaderProxyConfig{
|
||||||
Enable: true,
|
Enable: true,
|
||||||
Scheme: "socks5",
|
Scheme: "socks5",
|
||||||
Host: proxyListener.Addr().String(),
|
Host: proxyListener.Addr().String(),
|
||||||
Usr: usr,
|
Usr: usr,
|
||||||
Pwd: pwd,
|
Pwd: pwd,
|
||||||
})
|
}
|
||||||
|
if buildProxyConfig != nil {
|
||||||
|
globalProxyCfg = buildProxyConfig(globalProxyCfg)
|
||||||
|
}
|
||||||
|
downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg
|
||||||
|
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
|
URL: test.ExternalDownloadUrl,
|
||||||
|
}
|
||||||
|
if buildReqProxy != nil {
|
||||||
|
req.Proxy = buildReqProxy(&base.RequestProxy{
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
rr, err := downloader.Resolve(req)
|
rr, err := downloader.Resolve(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
if errHandler == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
errHandler(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Size: test.BuildSize,
|
Size: test.ExternalDownloadSize,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
{
|
{
|
||||||
Name: test.BuildName,
|
Name: test.ExternalDownloadName,
|
||||||
Path: "",
|
Path: "",
|
||||||
Size: test.BuildSize,
|
Size: test.ExternalDownloadSize,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -234,7 +400,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 1001)
|
time.Sleep(time.Millisecond * 1001)
|
||||||
err = downloader.Pause(id)
|
err = downloader.Pause(&TaskFilter{IDs: []string{id}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -258,7 +424,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
err = downloader.Continue(id)
|
err = downloader.Continue(&TaskFilter{IDs: []string{id}})
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -281,11 +447,11 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
|||||||
|
|
||||||
var httpCfg map[string]any
|
var httpCfg map[string]any
|
||||||
exits := downloader.getProtocolConfig("http", &httpCfg)
|
exits := downloader.getProtocolConfig("http", &httpCfg)
|
||||||
if exits {
|
if !exits {
|
||||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
t.Errorf("getProtocolConfig() got = %v, want %v", exits, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
storeCfg := &DownloaderStoreConfig{
|
storeCfg := &base.DownloaderStoreConfig{
|
||||||
DownloadDir: "./downloads",
|
DownloadDir: "./downloads",
|
||||||
ProtocolConfig: map[string]any{
|
ProtocolConfig: map[string]any{
|
||||||
"http": map[string]any{
|
"http": map[string]any{
|
||||||
@@ -319,3 +485,151 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
|||||||
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
|
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(nil)
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
downloader.Delete(nil, true)
|
||||||
|
downloader.Clear()
|
||||||
|
}()
|
||||||
|
|
||||||
|
reqs := make([]*base.Request, 0)
|
||||||
|
fileNames := make([]string, 0)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
reqs = append(reqs, req)
|
||||||
|
if i == 0 {
|
||||||
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
|
} else {
|
||||||
|
arr := strings.Split(test.DownloadName, ".")
|
||||||
|
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(reqs))
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter nil", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(nil)
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter nil task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter empty", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter empty task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: taskIds,
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
})
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{"xxx"},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
Statuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
Statuses: []base.Status{base.DownloadStatusError},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match notStatus", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
NotStatuses: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter match notStatus task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match notStatus", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
NotStatuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter not match notStatus task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match ids and status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
Statuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Errorf("GetTasksByFilter match ids and status task got = %v, want %v", len(tasks), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match ids and status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
Statuses: []base.Status{base.DownloadStatusError},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter not match ids and status task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
@@ -11,7 +12,6 @@ import (
|
|||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/dop251/goja_nodejs/eventloop"
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
gojaurl "github.com/dop251/goja_nodejs/url"
|
gojaurl "github.com/dop251/goja_nodejs/url"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,11 +106,11 @@ func (e *Engine) await(value any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) Close() {
|
func (e *Engine) Close() {
|
||||||
e.loop.Stop()
|
e.loop.StopNoWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ProxyURL *url.URL
|
ProxyConfig *base.DownloaderProxyConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEngine(cfg *Config) *Engine {
|
func NewEngine(cfg *Config) *Engine {
|
||||||
@@ -135,7 +135,7 @@ func NewEngine(cfg *Config) *Engine {
|
|||||||
if err := formdata.Enable(runtime); err != nil {
|
if err := formdata.Enable(runtime); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
|
if err := xhr.Enable(runtime, cfg.ProxyConfig.ToHandler()); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := runtime.RunString(polyfillScript); err != nil {
|
if _, err := runtime.RunString(polyfillScript); err != nil {
|
||||||
@@ -145,6 +145,10 @@ func NewEngine(cfg *Config) *Engine {
|
|||||||
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// polyfill window
|
||||||
|
if err := runtime.Set("window", runtime.GlobalObject()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
// polyfill window.location
|
// polyfill window.location
|
||||||
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
||||||
return
|
return
|
||||||
@@ -174,7 +178,15 @@ func resolveResult(value goja.Value) (any, error) {
|
|||||||
if err, ok := p.Result().Export().(error); ok {
|
if err, ok := p.Result().Export().(error); ok {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New(p.Result().String())
|
stack := p.Result().String()
|
||||||
|
result := p.Result()
|
||||||
|
if ro, ok := result.(*goja.Object); ok {
|
||||||
|
stackVal := ro.Get("stack")
|
||||||
|
if stackVal != nil && stackVal.String() != "" {
|
||||||
|
stack = stackVal.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New(stack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/test"
|
|
||||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
|
||||||
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,6 +13,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPolyfill(t *testing.T) {
|
func TestPolyfill(t *testing.T) {
|
||||||
@@ -26,6 +27,8 @@ func TestPolyfill(t *testing.T) {
|
|||||||
doTestPolyfill(t, "XMLHttpRequest")
|
doTestPolyfill(t, "XMLHttpRequest")
|
||||||
doTestPolyfill(t, "Blob")
|
doTestPolyfill(t, "Blob")
|
||||||
doTestPolyfill(t, "FormData")
|
doTestPolyfill(t, "FormData")
|
||||||
|
doTestPolyfill(t, "TextDecoder")
|
||||||
|
doTestPolyfill(t, "TextEncoder")
|
||||||
doTestPolyfill(t, "fetch")
|
doTestPolyfill(t, "fetch")
|
||||||
doTestPolyfill(t, "__gopeed_create_vm")
|
doTestPolyfill(t, "__gopeed_create_vm")
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,44 @@ async function testOctetStream(file){
|
|||||||
return await resp.text();
|
return await resp.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testRedirect() {
|
||||||
|
const url = host + '/redirect?num=3'
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
redirect: 'error',
|
||||||
|
}).then(()=>reject())
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
redirect: 'follow',
|
||||||
|
}).then((res) =>res.headers.has('location') && reject()).catch(() => reject())
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
redirect: 'manual',
|
||||||
|
}).then((res) => {
|
||||||
|
const location = res.headers.get('location');
|
||||||
|
location ? resolve(location) : reject()
|
||||||
|
}).catch(() => reject())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testResponseUrl() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', host+'/redirect?num=3');
|
||||||
|
xhr.onload = function(){
|
||||||
|
if (xhr.responseURL.includes('/redirect?num=0')){
|
||||||
|
resolve();
|
||||||
|
}else{
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function testFormData(file){
|
async function testFormData(file){
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('name', 'test');
|
formData.append('name', 'test');
|
||||||
@@ -80,6 +121,30 @@ async function testFormData(file){
|
|||||||
return await resp.json();
|
return await resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testHeader(){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', host+'/header');
|
||||||
|
xhr.setRequestHeader('X-Gopeed-Test', 'test1');
|
||||||
|
xhr.setRequestHeader('x-gopeed-test', 'test2');
|
||||||
|
xhr.setRequestHeader('x-Gopeed-test', 'test3');
|
||||||
|
xhr.onload = function(){
|
||||||
|
const testHeader1 = xhr.getResponseHeader("X-Gopeed-Test");
|
||||||
|
const testHeader2 = xhr.getResponseHeader("x-gopeed-test");
|
||||||
|
const testHeader3 = xhr.getResponseHeader("x-Gopeed-test");
|
||||||
|
const expect = 'test1, test2, test3';
|
||||||
|
const all = xhr.getAllResponseHeaders();
|
||||||
|
if(testHeader1 === expect && testHeader2 === expect && testHeader3 === expect
|
||||||
|
&& all.includes('X-Gopeed-Test: '+expect)){
|
||||||
|
resolve();
|
||||||
|
}else{
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function testProgress(){
|
function testProgress(){
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
@@ -172,6 +237,20 @@ function testTimeout(){
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
t.Run("testRedirect", func(t *testing.T) {
|
||||||
|
_, err := callTestFun(engine, "testRedirect")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("testResponseUrl", func(t *testing.T) {
|
||||||
|
_, err = callTestFun(engine, "testResponseUrl")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
jsFile, goFile, md5 := buildFile(t, engine.Runtime)
|
jsFile, goFile, md5 := buildFile(t, engine.Runtime)
|
||||||
result, err = callTestFun(engine, "testFormData", jsFile)
|
result, err = callTestFun(engine, "testFormData", jsFile)
|
||||||
@@ -190,14 +269,19 @@ function testTimeout(){
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
_, err = callTestFun(engine, "testHeader")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("header test failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = callTestFun(engine, "testProgress")
|
_, err = callTestFun(engine, "testProgress")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("progress test failed")
|
t.Fatal("progress test failed", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = callTestFun(engine, "testAbort")
|
_, err = callTestFun(engine, "testAbort")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("abort test failed")
|
t.Fatal("abort test failed", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = callTestFun(engine, "testTimeout")
|
_, err = callTestFun(engine, "testTimeout")
|
||||||
@@ -217,8 +301,16 @@ func doTestFetchWithProxy(t *testing.T, usr, pwd string) {
|
|||||||
|
|
||||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
engine := NewEngine(&Config{
|
||||||
engine := NewEngine(&Config{ProxyURL: util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)})
|
ProxyConfig: &base.DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
System: false,
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
|
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -320,6 +412,15 @@ func startServer() net.Listener {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for k, v := range r.Header {
|
||||||
|
if strings.HasPrefix(k, "X-Gopeed") {
|
||||||
|
w.Header().Set(k, strings.Join(v, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
mux.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) {
|
||||||
buf, _ := io.ReadAll(r.Body)
|
buf, _ := io.ReadAll(r.Body)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -359,6 +460,16 @@ func startServer() net.Listener {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
num := r.URL.Query().Get("num")
|
||||||
|
n, _ := strconv.Atoi(num)
|
||||||
|
if n > 0 {
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/redirect?num=%d", n-1), http.StatusFound)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
})
|
||||||
server.Handler = mux
|
server.Handler = mux
|
||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
return listener
|
return listener
|
||||||
|
|||||||
@@ -2,16 +2,19 @@ package xhr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
"errors"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,6 +26,12 @@ const (
|
|||||||
eventTimeout = "timeout"
|
eventTimeout = "timeout"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
redirectError = "error"
|
||||||
|
redirectFollow = "follow"
|
||||||
|
redirectManual = "manual"
|
||||||
|
)
|
||||||
|
|
||||||
type ProgressEvent struct {
|
type ProgressEvent struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
LengthComputable bool `json:"lengthComputable"`
|
LengthComputable bool `json:"lengthComputable"`
|
||||||
@@ -117,18 +126,26 @@ type XMLHttpRequestUpload struct {
|
|||||||
type XMLHttpRequest struct {
|
type XMLHttpRequest struct {
|
||||||
method string
|
method string
|
||||||
url string
|
url string
|
||||||
requestHeaders map[string]string
|
requestHeaders http.Header
|
||||||
responseHeaders map[string]string
|
responseHeaders http.Header
|
||||||
aborted bool
|
aborted bool
|
||||||
proxyUrl *url.URL
|
proxyHandler func(r *http.Request) (*url.URL, error)
|
||||||
|
|
||||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
WithCredentials bool `json:"withCredentials"`
|
||||||
Timeout int `json:"timeout"`
|
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||||
ReadyState int `json:"readyState"`
|
Timeout int `json:"timeout"`
|
||||||
Status int `json:"status"`
|
ReadyState int `json:"readyState"`
|
||||||
StatusText string `json:"statusText"`
|
Status int `json:"status"`
|
||||||
Response string `json:"response"`
|
StatusText string `json:"statusText"`
|
||||||
ResponseText string `json:"responseText"`
|
Response string `json:"response"`
|
||||||
|
ResponseText string `json:"responseText"`
|
||||||
|
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseURL
|
||||||
|
// https://xhr.spec.whatwg.org/#the-responseurl-attribute
|
||||||
|
ResponseUrl string `json:"responseURL"`
|
||||||
|
// extend fetch redirect
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#redirect
|
||||||
|
// https://fetch.spec.whatwg.org/#concept-request-redirect-mode
|
||||||
|
Redirect string `json:"redirect"`
|
||||||
*EventProp
|
*EventProp
|
||||||
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
|
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
|
||||||
}
|
}
|
||||||
@@ -136,13 +153,13 @@ type XMLHttpRequest struct {
|
|||||||
func (xhr *XMLHttpRequest) Open(method, url string) {
|
func (xhr *XMLHttpRequest) Open(method, url string) {
|
||||||
xhr.method = method
|
xhr.method = method
|
||||||
xhr.url = url
|
xhr.url = url
|
||||||
xhr.requestHeaders = make(map[string]string)
|
xhr.requestHeaders = make(http.Header)
|
||||||
xhr.responseHeaders = make(map[string]string)
|
xhr.responseHeaders = make(http.Header)
|
||||||
xhr.doReadystatechange(1)
|
xhr.doReadystatechange(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
||||||
xhr.requestHeaders[key] = value
|
xhr.requestHeaders.Add(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||||
@@ -152,31 +169,33 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
var (
|
var (
|
||||||
contentType string
|
contentType string
|
||||||
contentLength int64
|
contentLength int64
|
||||||
|
isStringBody bool
|
||||||
)
|
)
|
||||||
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
||||||
} else {
|
} else {
|
||||||
switch d.(type) {
|
switch v := d.(type) {
|
||||||
case string:
|
case string:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string)))
|
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
|
||||||
contentType = "text/plain;charset=UTF-8"
|
contentType = "text/plain;charset=UTF-8"
|
||||||
contentLength = int64(len(d.(string)))
|
contentLength = int64(len(v))
|
||||||
|
isStringBody = true
|
||||||
case *file.File:
|
case *file.File:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader)
|
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
contentLength = d.(*file.File).Size
|
contentLength = v.Size
|
||||||
case *formdata.FormData:
|
case *formdata.FormData:
|
||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
mw := NewMultipart(pw)
|
mw := NewMultipart(pw)
|
||||||
for _, e := range d.(*formdata.FormData).Entries() {
|
for _, e := range v.Entries() {
|
||||||
arr := e.([]any)
|
arr := e.([]any)
|
||||||
k := arr[0].(string)
|
k := arr[0].(string)
|
||||||
v := arr[1]
|
v := arr[1]
|
||||||
switch v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
mw.WriteField(k, v.(string))
|
mw.WriteField(k, v)
|
||||||
case *file.File:
|
case *file.File:
|
||||||
mw.WriteFile(k, v.(*file.File))
|
mw.WriteFile(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
@@ -193,27 +212,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
xhr.callOnerror()
|
xhr.callOnerror()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if contentType != "" {
|
req.Header = xhr.requestHeaders
|
||||||
|
// Only string body can specify Content-Type header by user
|
||||||
|
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
|
||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
if contentLength > 0 {
|
if contentLength > 0 {
|
||||||
req.ContentLength = contentLength
|
req.ContentLength = contentLength
|
||||||
}
|
}
|
||||||
for k, v := range xhr.requestHeaders {
|
transport := &http.Transport{
|
||||||
req.Header.Set(k, v)
|
Proxy: xhr.proxyHandler,
|
||||||
}
|
|
||||||
transport := &http.Transport{}
|
|
||||||
if xhr.proxyUrl != nil {
|
|
||||||
transport.Proxy = http.ProxyURL(xhr.proxyUrl)
|
|
||||||
}
|
}
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if xhr.Redirect == redirectManual {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
if xhr.Redirect == redirectError {
|
||||||
|
return errors.New("redirect failed")
|
||||||
|
}
|
||||||
|
if len(via) > 20 {
|
||||||
|
return errors.New("too many redirects")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// handle timeout error
|
// handle timeout error
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
var ne net.Error
|
||||||
|
if errors.As(err, &ne) && ne.Timeout() {
|
||||||
if xhr.Timeout > 0 {
|
if xhr.Timeout > 0 {
|
||||||
xhr.Upload.callOntimeout()
|
xhr.Upload.callOntimeout()
|
||||||
xhr.callOntimeout()
|
xhr.callOntimeout()
|
||||||
@@ -229,9 +259,12 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.Upload.callOnload()
|
xhr.Upload.callOnload()
|
||||||
}
|
}
|
||||||
for k, v := range resp.Header {
|
|
||||||
xhr.responseHeaders[k] = v[0]
|
responseUrl := resp.Request.URL
|
||||||
}
|
responseUrl.Fragment = ""
|
||||||
|
xhr.ResponseUrl = responseUrl.String()
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -260,7 +293,7 @@ func (xhr *XMLHttpRequest) Abort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
||||||
return xhr.responseHeaders[key]
|
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
||||||
@@ -268,7 +301,7 @@ func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
|||||||
for k, v := range xhr.responseHeaders {
|
for k, v := range xhr.responseHeaders {
|
||||||
buf.WriteString(k)
|
buf.WriteString(k)
|
||||||
buf.WriteString(": ")
|
buf.WriteString(": ")
|
||||||
buf.WriteString(v)
|
buf.WriteString(strings.Join(v, ", "))
|
||||||
buf.WriteString("\r\n")
|
buf.WriteString("\r\n")
|
||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
@@ -313,7 +346,7 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
|
|||||||
return data.String()
|
return data.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL, error)) error {
|
||||||
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
if len(call.Arguments) < 1 {
|
if len(call.Arguments) < 1 {
|
||||||
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
|
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
|
||||||
@@ -327,7 +360,7 @@ func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
|||||||
})
|
})
|
||||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
instance := &XMLHttpRequest{
|
instance := &XMLHttpRequest{
|
||||||
proxyUrl: proxyUrl,
|
proxyHandler: proxyHandler,
|
||||||
Upload: &XMLHttpRequestUpload{
|
Upload: &XMLHttpRequestUpload{
|
||||||
EventProp: &EventProp{
|
EventProp: &EventProp{
|
||||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||||
@@ -386,10 +419,9 @@ func (w *multipartWrapper) Size() int64 {
|
|||||||
w.statWriter.Close()
|
w.statWriter.Close()
|
||||||
size := int64(w.statBuffer.Len())
|
size := int64(w.statBuffer.Len())
|
||||||
for _, v := range w.fields {
|
for _, v := range w.fields {
|
||||||
switch v.(type) {
|
switch v := v.(type) {
|
||||||
case *file.File:
|
case *file.File:
|
||||||
f := v.(*file.File)
|
size += v.Size
|
||||||
size += f.Size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return size
|
return size
|
||||||
@@ -397,18 +429,17 @@ func (w *multipartWrapper) Size() int64 {
|
|||||||
|
|
||||||
func (w *multipartWrapper) Send() error {
|
func (w *multipartWrapper) Send() error {
|
||||||
for k, v := range w.fields {
|
for k, v := range w.fields {
|
||||||
switch v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
if err := w.writer.WriteField(k, v.(string)); err != nil {
|
if err := w.writer.WriteField(k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case *file.File:
|
case *file.File:
|
||||||
f := v.(*file.File)
|
fw, err := w.writer.CreateFormFile(k, v.Name)
|
||||||
fw, err := w.writer.CreateFormFile(k, f.Name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err = io.Copy(fw, f); err != nil {
|
if _, err = io.Copy(fw, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e<r;){var u=t.charCodeAt(e++);if(u>=55296&&u<=56319){if(e<r){var f=t.charCodeAt(e);56320==(64512&f)&&(++e,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o<e;){var n,i,a,s,u=t[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p<c;)h+=String.fromCharCode.apply(String,r.slice(p,p+=4096));return h};function b(){var e=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;e&&r&&(XMLHttpRequest.prototype.send=function(t){t instanceof Blob?(this.setRequestHeader("Content-Type",t.type),r.call(this,t)):r.call(this,t)});try{new File([],""),t.File=i.File,t.FileReader=i.FileReader}catch(e){try{t.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(e){t.File=function(t,e,r){var o=new Blob(t,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=e.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),t.Blob=f?i.Blob:p):l?(b(),t.Blob=h):function(){function e(t){for(var e=new Array(t.byteLength),r=new Uint8Array(t),o=e.length;o--;)e[o]=r[o];return e}function o(t){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<t.length;o+=3){var n=t[o],i=o+1<t.length,a=i?t[o+1]:0,s=o+2<t.length,u=s?t[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o<n;o++){var a=t[o];a instanceof h?t[o]=a._buffer:"string"==typeof a?t[o]=y(a):"DataView"===u(s=a)||f(i.DataView,s)?t[o]=e(a.buffer):c(a)?t[o]=e(a):t[o]=y(String(a))}var s;this._buffer=i.Uint8Array?function(t){for(var e=0,r=t.length;r--;)e+=t[r].length;for(var o=new Uint8Array(e),n=0,i=0;i<t.length;i++){var a=t[i];o.set(a,n),n+=a.byteLength||a.length}return o}(t):[].concat.apply([],t),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
(()=>{var e={725:function(e,t,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(e){"use strict";var t=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(e,t){return(t=document.createElement("a")).href=e,t},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=t&&t.prototype.append&&t.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(e){}function c(e){return e.map((function(e){if(e.buffer instanceof ArrayBuffer){var t=e.buffer;if(e.byteLength!==t.byteLength){var r=new Uint8Array(e.byteLength);r.set(new Uint8Array(t,e.byteOffset,e.byteLength)),t=r.buffer}return t}return e}))}function h(e,r){r=r||{};var o=new t;return c(e).forEach((function(e){o.append(e)})),r.type?o.getBlob(r.type):o.getBlob()}function d(e,t){return new o(c(e),t||{})}i.Blob&&(h.prototype=Blob.prototype,d.prototype=Blob.prototype);var p="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(e){for(var t=0,r=e.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);t<r;){var u=e.charCodeAt(t++);if(u>=55296&&u<=56319){if(t<r){var f=e.charCodeAt(t);56320==(64512&f)&&(++t,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+t/e.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},y="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(e){for(var t=e.length,r=[],o=0;o<t;){var n,i,a,s,u=e[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=t)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=e[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=e[o+1],i=e[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=e[o+1],i=e[o+2],a=e[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",d=0;d<c;)h+=String.fromCharCode.apply(String,r.slice(d,d+=4096));return h};function b(){var t=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;t&&r&&(XMLHttpRequest.prototype.send=function(e){e instanceof Blob?(this.setRequestHeader("Content-Type",e.type),r.call(this,e)):r.call(this,e)});try{new File([],""),e.File=i.File,e.FileReader=i.FileReader}catch(t){try{e.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(t){e.File=function(e,t,r){var o=new Blob(e,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=t.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),e.Blob=f?i.Blob:d):l?(b(),e.Blob=h):function(){function t(e){for(var t=new Array(e.byteLength),r=new Uint8Array(e),o=t.length;o--;)t[o]=r[o];return t}function o(e){for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<e.length;o+=3){var n=e[o],i=o+1<e.length,a=i?e[o+1]:0,s=o+2<e.length,u=s?e[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(t[f],t[l],t[c],t[h])}return r.join("")}var s=Object.create||function(e){function t(){}return t.prototype=e,new t};function u(e){return Object.prototype.toString.call(e).slice(8,-1)}function f(e,t){return"object"==typeof e&&Object.prototype.isPrototypeOf.call(e.prototype,t)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(e){return t=l,r=u(e),-1!==t.indexOf(r)||f(i.ArrayBuffer,e);var t,r}function h(e,r){r=null==r?{}:r;for(var o=0,n=(e=e?e.slice():[]).length;o<n;o++){var a=e[o];a instanceof h?e[o]=a._buffer:"string"==typeof a?e[o]=p(a):"DataView"===u(s=a)||f(i.DataView,s)?e[o]=t(a.buffer):c(a)?e[o]=t(a):e[o]=p(String(a))}var s;this._buffer=i.Uint8Array?function(e){for(var t=0,r=e.length;r--;)t+=e[r].length;for(var o=new Uint8Array(t),n=0,i=0;i<e.length;i++){var a=e[i];o.set(a,n),n+=a.byteLength||a.length}return o}(e):[].concat.apply([],e),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
||||||
+1169
-15
File diff suppressed because it is too large.
Load diff
@@ -7,16 +7,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "webpack --mode production --watch",
|
"dev": "webpack --mode production --watch",
|
||||||
"build": "webpack --mode production"
|
"build": "webpack --mode production",
|
||||||
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.75.0",
|
||||||
"webpack-cli": "^5.0.1"
|
"webpack-cli": "^5.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"blob-polyfill": "^7.0.20220408",
|
"blob-polyfill": "^7.0.20220408",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"fastestsmallesttextencoderdecoder": "^1.0.22",
|
||||||
|
"whatwg-fetch": "^3.6.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
diff --git a/node_modules/whatwg-fetch/dist/fetch.umd.js b/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||||
|
index 7a0d852..604691e 100644
|
||||||
|
--- a/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||||
|
+++ b/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||||
|
@@ -394,6 +394,7 @@
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
this.referrer = null;
|
||||||
|
+ this.redirect = options.redirect || 'follow'
|
||||||
|
|
||||||
|
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||||
|
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||||
|
@@ -606,6 +607,10 @@
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ if ('redirect' in xhr) {
|
||||||
|
+ xhr.redirect = request.redirect
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||||
|
var names = [];
|
||||||
|
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||||
|
diff --git a/node_modules/whatwg-fetch/fetch.js b/node_modules/whatwg-fetch/fetch.js
|
||||||
|
index f39a983..d1fc903 100644
|
||||||
|
--- a/node_modules/whatwg-fetch/fetch.js
|
||||||
|
+++ b/node_modules/whatwg-fetch/fetch.js
|
||||||
|
@@ -388,6 +388,7 @@ export function Request(input, options) {
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
this.referrer = null
|
||||||
|
+ this.redirect = options.redirect || 'follow'
|
||||||
|
|
||||||
|
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||||
|
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||||
|
@@ -600,6 +601,10 @@ export function fetch(input, init) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ if ('redirect' in xhr) {
|
||||||
|
+ xhr.redirect = request.redirect
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||||
|
var names = [];
|
||||||
|
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import "./blob/index.js"
|
import "./blob/index.js"
|
||||||
import "./crypto/index.js"
|
import "./crypto/index.js"
|
||||||
|
// polyfill TextEncoder
|
||||||
|
import 'fastestsmallesttextencoderdecoder';
|
||||||
import "./fetch/index.js"
|
import "./fetch/index.js"
|
||||||
+43
-12
@@ -38,7 +38,7 @@ const (
|
|||||||
EventOnResolve ActivationEvent = "onResolve"
|
EventOnResolve ActivationEvent = "onResolve"
|
||||||
EventOnStart ActivationEvent = "onStart"
|
EventOnStart ActivationEvent = "onStart"
|
||||||
EventOnError ActivationEvent = "onError"
|
EventOnError ActivationEvent = "onError"
|
||||||
//EventOnDone ActivationEvent = "onDone"
|
EventOnDone ActivationEvent = "onDone"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
||||||
@@ -155,7 +155,13 @@ func (d *Downloader) DeleteExtension(identity string) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d.storage.Delete(bucketExtension, identity)
|
if err = d.storage.Delete(bucketExtension, identity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = d.storage.Delete(bucketExtensionStorage, identity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) GetExtensions() []*Extension {
|
func (d *Downloader) GetExtensions() []*Extension {
|
||||||
@@ -199,7 +205,7 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
proxyOptions := transport.ProxyOptions{}
|
proxyOptions := transport.ProxyOptions{}
|
||||||
proxyUrl := d.cfg.ProxyUrl()
|
proxyUrl := d.cfg.DownloaderStoreConfig.Proxy.ToUrl()
|
||||||
if proxyUrl != nil {
|
if proxyUrl != nil {
|
||||||
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
|
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
|
||||||
proxyOptions.Username = proxyUrl.User.Username()
|
proxyOptions.Username = proxyUrl.User.Username()
|
||||||
@@ -298,6 +304,17 @@ func (d *Downloader) triggerOnError(task *Task, err error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) triggerOnDone(task *Task) {
|
||||||
|
doTrigger(d,
|
||||||
|
EventOnDone,
|
||||||
|
task.Meta.Req,
|
||||||
|
&OnErrorContext{
|
||||||
|
Task: NewExtensionTask(d, task),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
||||||
// init extension global object
|
// init extension global object
|
||||||
gopeed := &Instance{
|
gopeed := &Instance{
|
||||||
@@ -341,7 +358,7 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
|
|||||||
req.Labels = make(map[string]string)
|
req.Labels = make(map[string]string)
|
||||||
}
|
}
|
||||||
engine := engine.NewEngine(&engine.Config{
|
engine := engine.NewEngine(&engine.Config{
|
||||||
ProxyURL: d.cfg.ProxyUrl(),
|
ProxyConfig: d.cfg.Proxy,
|
||||||
})
|
})
|
||||||
defer engine.Close()
|
defer engine.Close()
|
||||||
err = engine.Runtime.Set("gopeed", gopeed)
|
err = engine.Runtime.Set("gopeed", gopeed)
|
||||||
@@ -605,9 +622,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
|
|||||||
h.register(EventOnError, fn)
|
h.register(EventOnError, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (h InstanceEvents) OnDone(fn goja.Callable) {
|
func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||||
// h.register(HookEventOnDone, fn)
|
h.register(EventOnDone, fn)
|
||||||
//}
|
}
|
||||||
|
|
||||||
type ExtensionInfo struct {
|
type ExtensionInfo struct {
|
||||||
Identity string `json:"identity"`
|
Identity string `json:"identity"`
|
||||||
@@ -681,25 +698,39 @@ type OnErrorContext struct {
|
|||||||
Error error `json:"error"`
|
Error error `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtensionTask struct {
|
type OnDoneContext struct {
|
||||||
*Task
|
Task *Task `json:"task"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionTask is a wrapper of Task, it's used to interact with extension scripts.
|
||||||
|
// Avoid extension scripts modifying task directly, use ExtensionTask to encapsulate task,
|
||||||
|
// only some fields can be modified, such as request info.
|
||||||
|
type ExtensionTask struct {
|
||||||
download *Downloader
|
download *Downloader
|
||||||
|
|
||||||
|
*Task
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||||
|
// restricts extension scripts to only modify request info
|
||||||
|
newTask := task.clone()
|
||||||
|
newTask.Meta.Req = task.Meta.Req
|
||||||
return &ExtensionTask{
|
return &ExtensionTask{
|
||||||
Task: task.clone(),
|
|
||||||
download: download,
|
download: download,
|
||||||
|
Task: newTask,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExtensionTask) Continue() error {
|
func (t *ExtensionTask) Continue() error {
|
||||||
return t.download.Continue(t.ID)
|
return t.download.Continue(&TaskFilter{
|
||||||
|
IDs: []string{t.ID},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExtensionTask) Pause() error {
|
func (t *ExtensionTask) Pause() error {
|
||||||
return t.download.Pause(t.ID)
|
return t.download.Pause(&TaskFilter{
|
||||||
|
IDs: []string{t.ID},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSettings(settings []*Setting) map[string]any {
|
func parseSettings(settings []*Setting) map[string]any {
|
||||||
|
|||||||
@@ -247,8 +247,45 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// extension on error modify url and continue download
|
// extension on error modify url and continue download
|
||||||
task := downloader.GetTask(id)
|
task := downloader.GetTask(id)
|
||||||
if task.Meta.Req.URL != "https://github.com" {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_Extension_OnDone(t *testing.T) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_done", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyFinally {
|
||||||
|
errCh <- event.Err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
id, err := downloader.CreateDirect(&base.Request{
|
||||||
|
URL: "https://github.com",
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-errCh:
|
||||||
|
break
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
err = errors.New("timeout")
|
||||||
|
}
|
||||||
|
// wait for script execution
|
||||||
|
time.Sleep(time.Millisecond * 3000)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("extension on done download error: " + err.Error())
|
||||||
|
}
|
||||||
|
// extension on error modify url and continue download
|
||||||
|
task := downloader.GetTask(id)
|
||||||
|
if task.Meta.Req.Labels["modified"] != "true" {
|
||||||
|
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
|
||||||
}
|
}
|
||||||
if task.Status != base.DownloadStatusDone {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except status is done, actual: %s", task.Status)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
|||||||
+73
-66
@@ -1,15 +1,14 @@
|
|||||||
package download
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
||||||
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
"net/url"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -21,19 +20,21 @@ type ResolveResult struct {
|
|||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||||
Status base.Status `json:"status"`
|
Status base.Status `json:"status"`
|
||||||
Protocol string `json:"protocol"`
|
Uploading bool `json:"uploading"`
|
||||||
Progress *Progress `json:"progress"`
|
Progress *Progress `json:"progress"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
fetcherBuilder fetcher.FetcherBuilder
|
fetcherManager fetcher.FetcherManager
|
||||||
fetcher fetcher.Fetcher
|
fetcher fetcher.Fetcher
|
||||||
timer *util.Timer
|
timer *util.Timer
|
||||||
statusLock *sync.Mutex
|
statusLock *sync.Mutex
|
||||||
lock *sync.Mutex
|
lock *sync.Mutex
|
||||||
speedArr []int64
|
speedArr []int64
|
||||||
|
uploadSpeedArr []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTask() *Task {
|
func NewTask() *Task {
|
||||||
@@ -49,55 +50,100 @@ func NewTask() *Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the display name of the task.
|
||||||
|
func (t *Task) Name() string {
|
||||||
|
// Custom name first
|
||||||
|
if t.Meta.Opts.Name != "" {
|
||||||
|
return t.Meta.Opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task is not resolved, parse the name from the URL
|
||||||
|
if t.Meta.Res == nil {
|
||||||
|
fallbackName := "unknown"
|
||||||
|
if t.fetcherManager == nil {
|
||||||
|
return fallbackName
|
||||||
|
}
|
||||||
|
parseName := t.fetcherManager.ParseName(t.Meta.Req.URL)
|
||||||
|
if parseName == "" {
|
||||||
|
return fallbackName
|
||||||
|
}
|
||||||
|
return parseName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task is a folder
|
||||||
|
if t.Meta.Res.Name != "" {
|
||||||
|
return t.Meta.Res.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the name of the first file
|
||||||
|
return t.Meta.Res.Files[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) MarshalJSON() ([]byte, error) {
|
||||||
|
type rawTaskType Task
|
||||||
|
jsonTask := struct {
|
||||||
|
rawTaskType
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{
|
||||||
|
rawTaskType(*t),
|
||||||
|
t.Name(),
|
||||||
|
}
|
||||||
|
return json.Marshal(jsonTask)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) updateStatus(status base.Status) {
|
func (t *Task) updateStatus(status base.Status) {
|
||||||
t.UpdatedAt = time.Now()
|
t.UpdatedAt = time.Now()
|
||||||
t.Status = status
|
t.Status = status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) clone() *Task {
|
func (t *Task) clone() *Task {
|
||||||
return &Task{
|
return util.DeepClone(t)
|
||||||
ID: t.ID,
|
|
||||||
Meta: t.Meta,
|
|
||||||
Status: t.Status,
|
|
||||||
Progress: t.Progress,
|
|
||||||
CreatedAt: t.CreatedAt,
|
|
||||||
UpdatedAt: t.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
|
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
||||||
t.speedArr = append(t.speedArr, downloaded)
|
speedArr = append(speedArr, downloaded)
|
||||||
if len(t.speedArr) > 6 {
|
if len(speedArr) > 6 {
|
||||||
t.speedArr = t.speedArr[1:]
|
speedArr = speedArr[1:]
|
||||||
}
|
}
|
||||||
var total int64
|
var total int64
|
||||||
for _, v := range t.speedArr {
|
for _, v := range speedArr {
|
||||||
total += v
|
total += v
|
||||||
}
|
}
|
||||||
return int64(float64(total) / float64(len(t.speedArr)) / usedTime)
|
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskFilter struct {
|
||||||
|
IDs []string
|
||||||
|
Statuses []base.Status
|
||||||
|
NotStatuses []base.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TaskFilter) IsEmpty() bool {
|
||||||
|
return len(f.IDs) == 0 && len(f.Statuses) == 0 && len(f.NotStatuses) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloaderConfig struct {
|
type DownloaderConfig struct {
|
||||||
Controller *controller.Controller
|
Controller *controller.Controller
|
||||||
FetchBuilders []fetcher.FetcherBuilder
|
FetchManagers []fetcher.FetcherManager
|
||||||
|
|
||||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||||
Storage Storage
|
Storage Storage
|
||||||
StorageDir string
|
StorageDir string
|
||||||
|
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
*DownloaderStoreConfig
|
*base.DownloaderStoreConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
||||||
if cfg.Controller == nil {
|
if cfg.Controller == nil {
|
||||||
cfg.Controller = controller.NewController()
|
cfg.Controller = controller.NewController()
|
||||||
}
|
}
|
||||||
if len(cfg.FetchBuilders) == 0 {
|
if len(cfg.FetchManagers) == 0 {
|
||||||
cfg.FetchBuilders = []fetcher.FetcherBuilder{
|
cfg.FetchManagers = []fetcher.FetcherManager{
|
||||||
new(http.FetcherBuilder),
|
new(http.FetcherManager),
|
||||||
new(bt.FetcherBuilder),
|
new(bt.FetcherManager),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.RefreshInterval == 0 {
|
if cfg.RefreshInterval == 0 {
|
||||||
@@ -108,42 +154,3 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
|||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
|
||||||
type DownloaderStoreConfig struct {
|
|
||||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
|
||||||
|
|
||||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
|
||||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
|
||||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
|
||||||
Extra map[string]any `json:"extra"`
|
|
||||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
|
||||||
if cfg.MaxRunning == 0 {
|
|
||||||
cfg.MaxRunning = 5
|
|
||||||
}
|
|
||||||
if cfg.Proxy == nil {
|
|
||||||
cfg.Proxy = &DownloaderProxyConfig{}
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *DownloaderStoreConfig) ProxyUrl() *url.URL {
|
|
||||||
if cfg.Proxy == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if cfg.Proxy.Enable == false || cfg.Proxy.Scheme == "" || cfg.Proxy.Host == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return util.BuildProxyUrl(cfg.Proxy.Scheme, cfg.Proxy.Host, cfg.Proxy.Usr, cfg.Proxy.Pwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloaderProxyConfig struct {
|
|
||||||
Enable bool `json:"enable"`
|
|
||||||
Scheme string `json:"scheme"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Usr string `json:"usr"`
|
|
||||||
Pwd string `json:"pwd"`
|
|
||||||
}
|
|
||||||
@@ -69,7 +69,9 @@ func (n *MemStorage) Setup(buckets []string) error {
|
|||||||
func (n *MemStorage) Put(bucket string, key string, v any) error {
|
func (n *MemStorage) Put(bucket string, key string, v any) error {
|
||||||
n.lock.Lock()
|
n.lock.Lock()
|
||||||
defer n.lock.Unlock()
|
defer n.lock.Unlock()
|
||||||
memData[bucket][key] = v
|
if bucketData, ok := memData[bucket]; ok {
|
||||||
|
bucketData[key] = v
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
gopeed.events.onDone(async function (ctx) {
|
||||||
|
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||||
|
ctx.task.meta.req.labels['modified'] = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "on-done",
|
||||||
|
"title": "gopeed extension on done event test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onDone",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ gopeed.events.onError(async function (ctx) {
|
|||||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||||
gopeed.logger.info("error", ctx.error);
|
gopeed.logger.info("error", ctx.error);
|
||||||
ctx.task.meta.req.url = "https://github.com";
|
ctx.task.meta.req.url = "https://github.com";
|
||||||
ctx.task.pause();
|
|
||||||
ctx.task.continue();
|
ctx.task.continue();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4,11 +4,16 @@ type ReqExtra struct {
|
|||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats for download
|
// Stats for torrent
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
// bt stats
|
|
||||||
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
||||||
TotalPeers int `json:"totalPeers"`
|
TotalPeers int `json:"totalPeers"`
|
||||||
ActivePeers int `json:"activePeers"`
|
ActivePeers int `json:"activePeers"`
|
||||||
ConnectedSeeders int `json:"connectedSeeders"`
|
ConnectedSeeders int `json:"connectedSeeders"`
|
||||||
|
// Total seed bytes
|
||||||
|
SeedBytes int64 `json:"seedBytes"`
|
||||||
|
// Seed ratio
|
||||||
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
|
// Total seed time
|
||||||
|
SeedTime int64 `json:"seedTime"`
|
||||||
}
|
}
|
||||||
+105
-31
@@ -8,8 +8,20 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func Info(w http.ResponseWriter, r *http.Request) {
|
||||||
|
info := map[string]any{
|
||||||
|
"version": base.Version,
|
||||||
|
"runtime": runtime.Version(),
|
||||||
|
"os": runtime.GOOS,
|
||||||
|
"arch": runtime.GOARCH,
|
||||||
|
"inDocker": base.InDocker == "true",
|
||||||
|
}
|
||||||
|
WriteJson(w, model.NewOkResult(info))
|
||||||
|
}
|
||||||
|
|
||||||
func Resolve(w http.ResponseWriter, r *http.Request) {
|
func Resolve(w http.ResponseWriter, r *http.Request) {
|
||||||
var req base.Request
|
var req base.Request
|
||||||
if ReadJson(r, w, &req) {
|
if ReadJson(r, w, &req) {
|
||||||
@@ -45,14 +57,44 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.CreateTaskBatch
|
||||||
|
if ReadJson(r, w, &req) {
|
||||||
|
if len(req.Reqs) == 0 {
|
||||||
|
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
||||||
|
if err != nil {
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJson(w, model.NewOkResult(taskIds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
if taskId == "" {
|
WriteJson(w, errResult)
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Pause(taskId); err != nil {
|
|
||||||
|
if err := Downloader.Pause(filter); err != nil {
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJson(w, model.NewNilResult())
|
||||||
|
}
|
||||||
|
|
||||||
|
func PauseTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filter, errResult := parseFilter(r)
|
||||||
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Downloader.Pause(filter); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,29 +102,27 @@ func PauseTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
if taskId == "" {
|
WriteJson(w, errResult)
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Continue(taskId); err != nil {
|
|
||||||
|
if err := Downloader.Continue(filter); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WriteJson(w, model.NewNilResult())
|
WriteJson(w, model.NewNilResult())
|
||||||
}
|
}
|
||||||
|
|
||||||
func PauseAllTask(w http.ResponseWriter, r *http.Request) {
|
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := Downloader.PauseAll(); err != nil {
|
filter, errResult := parseFilter(r)
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WriteJson(w, model.NewNilResult())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
|
if err := Downloader.Continue(filter); err != nil {
|
||||||
if err := Downloader.ContinueAll(); err != nil {
|
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -90,14 +130,14 @@ func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
force := r.FormValue("force")
|
WriteJson(w, errResult)
|
||||||
if taskId == "" {
|
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Delete(taskId, force == "true"); err != nil {
|
force := r.FormValue("force")
|
||||||
|
|
||||||
|
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -105,10 +145,14 @@ func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
filter, errResult := parseFilter(r)
|
||||||
status := r.Form["status"]
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
force := r.FormValue("force")
|
force := r.FormValue("force")
|
||||||
if err := Downloader.DeleteByStatues(convertStatues(status), force == "true"); err != nil {
|
|
||||||
|
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -131,9 +175,13 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
filter, errResult := parseFilter(r)
|
||||||
status := r.Form["status"]
|
if errResult != nil {
|
||||||
tasks := Downloader.GetTasksByStatues(convertStatues(status))
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := Downloader.GetTasksByFilter(filter)
|
||||||
WriteJson(w, model.NewOkResult(tasks))
|
WriteJson(w, model.NewOkResult(tasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +190,7 @@ func GetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PutConfig(w http.ResponseWriter, r *http.Request) {
|
func PutConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
var cfg download.DownloaderStoreConfig
|
var cfg base.DownloaderStoreConfig
|
||||||
if ReadJson(r, w, &cfg) {
|
if ReadJson(r, w, &cfg) {
|
||||||
if err := Downloader.PutConfig(&cfg); err != nil {
|
if err := Downloader.PutConfig(&cfg); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
@@ -296,6 +344,32 @@ func GetStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
WriteJson(w, model.NewOkResult(statsResult))
|
WriteJson(w, model.NewOkResult(statsResult))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseIdFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
taskId := vars["id"]
|
||||||
|
if taskId == "" {
|
||||||
|
return nil, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &download.TaskFilter{
|
||||||
|
IDs: []string{taskId},
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return nil, model.NewErrorResult(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &download.TaskFilter{
|
||||||
|
IDs: r.Form["id"],
|
||||||
|
Statuses: convertStatues(r.Form["status"]),
|
||||||
|
NotStatuses: convertStatues(r.Form["notStatus"]),
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertStatues(statues []string) []base.Status {
|
func convertStatues(statues []string) []base.Status {
|
||||||
result := make([]base.Status, 0)
|
result := make([]base.Status, 0)
|
||||||
for _, status := range statues {
|
for _, status := range statues {
|
||||||
@@ -309,7 +383,7 @@ func writeError(w http.ResponseWriter, msg string) {
|
|||||||
w.Write([]byte(msg))
|
w.Write([]byte(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServerConfig() *download.DownloaderStoreConfig {
|
func getServerConfig() *base.DownloaderStoreConfig {
|
||||||
cfg, _ := Downloader.GetConfig()
|
cfg, _ := Downloader.GetConfig()
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type Command struct {
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
Params any `json:"params"`
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,12 +14,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StartConfig struct {
|
type StartConfig struct {
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
RefreshInterval int `json:"refreshInterval"`
|
RefreshInterval int `json:"refreshInterval"`
|
||||||
Storage Storage `json:"storage"`
|
Storage Storage `json:"storage"`
|
||||||
StorageDir string `json:"storageDir"`
|
StorageDir string `json:"storageDir"`
|
||||||
ApiToken string `json:"apiToken"`
|
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||||
|
ApiToken string `json:"apiToken"`
|
||||||
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
@@ -7,3 +7,8 @@ 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"`
|
||||||
|
}
|
||||||
+23
-10
@@ -19,31 +19,42 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
srv *http.Server
|
srv *http.Server
|
||||||
|
runningPort int
|
||||||
|
|
||||||
Downloader *download.Downloader
|
Downloader *download.Downloader
|
||||||
)
|
)
|
||||||
|
|
||||||
func Start(startCfg *model.StartConfig) (int, error) {
|
func Start(startCfg *model.StartConfig) (port int, err error) {
|
||||||
srv, listener, err := BuildServer(startCfg)
|
// avoid repeat start
|
||||||
|
if srv != nil {
|
||||||
|
return runningPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener net.Listener
|
||||||
|
srv, listener, err = BuildServer(startCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
port := 0
|
|
||||||
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
|
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
|
||||||
port = addr.Port
|
port = addr.Port
|
||||||
|
runningPort = port
|
||||||
}
|
}
|
||||||
return port, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func Stop() {
|
func Stop() {
|
||||||
|
defer func() {
|
||||||
|
srv = nil
|
||||||
|
}()
|
||||||
|
|
||||||
if srv != nil {
|
if srv != nil {
|
||||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||||
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
||||||
@@ -88,12 +99,14 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
var r = mux.NewRouter()
|
var r = mux.NewRouter()
|
||||||
|
r.Methods(http.MethodGet).Path("/api/v1/info").HandlerFunc(Info)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
|
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
||||||
|
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
||||||
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseTasks)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueTasks)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
|
|
||||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
||||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
||||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
||||||
@@ -141,7 +154,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
defer func() {
|
defer func() {
|
||||||
if v := recover(); v != nil {
|
if v := recover(); v != nil {
|
||||||
err := errors.WithStack(fmt.Errorf("%v", v))
|
err := errors.WithStack(fmt.Errorf("%v", v))
|
||||||
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
|
Downloader.Logger.Error().Stack().Err(err).Msgf("http server panic: %s %s", r.Method, r.RequestURI)
|
||||||
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
|
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
+31
-12
@@ -63,6 +63,18 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestInfo(t *testing.T) {
|
||||||
|
matchKeys := []string{"version", "runtime", "os", "arch", "inDocker"}
|
||||||
|
doTest(func() {
|
||||||
|
resp := httpRequestCheckOk[map[string]any](http.MethodGet, "/api/v1/info", nil)
|
||||||
|
for _, key := range matchKeys {
|
||||||
|
if _, ok := resp[key]; !ok {
|
||||||
|
t.Errorf("Info() missing key = %v", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolve(t *testing.T) {
|
func TestResolve(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||||
@@ -125,6 +137,21 @@ func TestCreateDirectTask(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||||
|
doTest(func() {
|
||||||
|
reqs := make([]*base.Request, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
reqs = append(reqs, createReq.Req)
|
||||||
|
}
|
||||||
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
||||||
|
Reqs: reqs,
|
||||||
|
})
|
||||||
|
if len(taskIds) != len(reqs) {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -324,7 +351,7 @@ func TestGetTasks(t *testing.T) {
|
|||||||
|
|
||||||
func TestGetAndPutConfig(t *testing.T) {
|
func TestGetAndPutConfig(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
cfg := httpRequestCheckOk[*download.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||||
cfg.DownloadDir = "./download"
|
cfg.DownloadDir = "./download"
|
||||||
cfg.Extra = map[string]any{
|
cfg.Extra = map[string]any{
|
||||||
"serverConfig": &Config{
|
"serverConfig": &Config{
|
||||||
@@ -335,7 +362,7 @@ func TestGetAndPutConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/config", cfg)
|
httpRequestCheckOk[any](http.MethodPut, "/api/v1/config", cfg)
|
||||||
|
|
||||||
newCfg := httpRequestCheckOk[*download.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
newCfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||||
if !test.JsonEqual(cfg, newCfg) {
|
if !test.JsonEqual(cfg, newCfg) {
|
||||||
t.Errorf("GetAndPutConfig() got = %v, want %v", test.ToJson(newCfg), test.ToJson(cfg))
|
t.Errorf("GetAndPutConfig() got = %v, want %v", test.ToJson(newCfg), test.ToJson(cfg))
|
||||||
}
|
}
|
||||||
@@ -555,16 +582,8 @@ func doTest(handler func()) {
|
|||||||
Downloader.Clear()
|
Downloader.Clear()
|
||||||
}()
|
}()
|
||||||
defer func() {
|
defer func() {
|
||||||
Downloader.PauseAll()
|
Downloader.Pause(nil)
|
||||||
|
Downloader.Delete(nil, true)
|
||||||
tasks := Downloader.GetTasks()
|
|
||||||
taskIds := make([]string, len(tasks))
|
|
||||||
for i, task := range tasks {
|
|
||||||
taskIds[i] = task.ID
|
|
||||||
}
|
|
||||||
for _, id := range taskIds {
|
|
||||||
Downloader.Delete(id, true)
|
|
||||||
}
|
|
||||||
os.RemoveAll(cfg.StorageDir)
|
os.RemoveAll(cfg.StorageDir)
|
||||||
}()
|
}()
|
||||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||||
|
|||||||
@@ -12,3 +12,17 @@ func MapToStruct(s any, v any) error {
|
|||||||
}
|
}
|
||||||
return json.Unmarshal(b, v)
|
return json.Unmarshal(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeepClone[T any](v *T) *T {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var t T
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
json.Unmarshal(b, &t)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeepClone(t *testing.T) {
|
||||||
|
type user struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
|
||||||
|
v int
|
||||||
|
}
|
||||||
|
|
||||||
|
type args[T any] struct {
|
||||||
|
v *T
|
||||||
|
}
|
||||||
|
type testCase[T any] struct {
|
||||||
|
name string
|
||||||
|
args args[T]
|
||||||
|
want *T
|
||||||
|
}
|
||||||
|
tests := []testCase[user]{
|
||||||
|
{
|
||||||
|
name: "case 1",
|
||||||
|
args: args[user]{
|
||||||
|
v: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case 2",
|
||||||
|
args: args[user]{
|
||||||
|
v: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
v: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := DeepClone(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("DeepClone() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-7
@@ -2,23 +2,19 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FileSchema = "FILE"
|
|
||||||
|
|
||||||
func ParseSchema(url string) string {
|
func ParseSchema(url string) string {
|
||||||
index := strings.Index(url, ":")
|
index := strings.Index(url, ":")
|
||||||
// if no schema or windows path like C:\a.txt, return FILE
|
// if no schema or windows path like C:\a.txt, return FILE
|
||||||
if index == -1 || index == 1 {
|
if index == -1 || index == 1 {
|
||||||
return FileSchema
|
return ""
|
||||||
}
|
}
|
||||||
schema := url[:index]
|
schema := url[:index]
|
||||||
if schema == "data" {
|
|
||||||
schema, _ = ParseDataUri(url)
|
|
||||||
}
|
|
||||||
return strings.ToUpper(schema)
|
return strings.ToUpper(schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +27,6 @@ func ParseDataUri(uri string) (string, []byte) {
|
|||||||
}
|
}
|
||||||
mime := matches[1]
|
mime := matches[1]
|
||||||
base64Data := matches[2]
|
base64Data := matches[2]
|
||||||
// 解码Base64数据
|
|
||||||
data, err := base64.StdEncoding.DecodeString(base64Data)
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -51,3 +46,14 @@ func BuildProxyUrl(scheme, host, usr, pwd string) *url.URL {
|
|||||||
Host: host,
|
Host: host,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyUrlToHandler gets the proxy handler from the proxy url.
|
||||||
|
func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error) {
|
||||||
|
if proxyUrl == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if proxyUrl.Scheme == "system" {
|
||||||
|
return http.ProxyFromEnvironment
|
||||||
|
}
|
||||||
|
return http.ProxyURL(proxyUrl)
|
||||||
|
}
|
||||||
@@ -41,21 +41,21 @@ func TestParseSchema(t *testing.T) {
|
|||||||
args: args{
|
args: args{
|
||||||
url: "./url.go",
|
url: "./url.go",
|
||||||
},
|
},
|
||||||
want: "FILE",
|
want: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "data-uri",
|
name: "data-uri",
|
||||||
args: args{
|
args: args{
|
||||||
url: "data:application/x-bittorrent;base64,test",
|
url: "data:application/x-bittorrent;base64,test",
|
||||||
},
|
},
|
||||||
want: "APPLICATION/X-BITTORRENT",
|
want: "DATA",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "windows-path",
|
name: "windows-path",
|
||||||
args: args{
|
args: args{
|
||||||
url: "D:\\bt.torrent",
|
url: "D:\\bt.torrent",
|
||||||
},
|
},
|
||||||
want: "FILE",
|
want: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdkVersion 34
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
package="com.gopeed">
|
package="com.gopeed">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="Gopeed">
|
android:label="Gopeed"
|
||||||
|
android:extractNativeLibs="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 63 KiB |
@@ -13,6 +13,7 @@
|
|||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
B44F54F47E581A39546303C9 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 029EF293EAA92EB3D18BBF19 /* libresolv.tbd */; };
|
||||||
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D081C25B294826C0006EB10B /* libc++.tbd */; };
|
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D081C25B294826C0006EB10B /* libc++.tbd */; };
|
||||||
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; };
|
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; };
|
||||||
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; };
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */,
|
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */,
|
||||||
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */,
|
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */,
|
||||||
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */,
|
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */,
|
||||||
|
B44F54F47E581A39546303C9 /* libresolv.tbd in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -130,6 +133,7 @@
|
|||||||
D0E623A729482D030001185E /* Libgopeed.xcframework */,
|
D0E623A729482D030001185E /* Libgopeed.xcframework */,
|
||||||
D081C25B294826C0006EB10B /* libc++.tbd */,
|
D081C25B294826C0006EB10B /* libc++.tbd */,
|
||||||
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */,
|
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */,
|
||||||
|
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|||||||
@@ -129,12 +129,20 @@ 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() async {
|
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/pause"), null);
|
return _parse(
|
||||||
|
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
|
||||||
|
"id": ids,
|
||||||
|
}),
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> continueAllTasks() async {
|
Future<void> continueAllTasks(List<String>? ids) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null);
|
return _parse(
|
||||||
|
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
|
||||||
|
"id": ids,
|
||||||
|
}),
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteTask(String id, bool force) async {
|
Future<void> deleteTask(String id, bool force) async {
|
||||||
@@ -142,6 +150,15 @@ Future<void> deleteTask(String id, bool force) async {
|
|||||||
() => _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 {
|
||||||
|
return _parse(
|
||||||
|
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
|
||||||
|
"id": ids,
|
||||||
|
"force": force,
|
||||||
|
}),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
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));
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class DownloaderConfig {
|
|||||||
|
|
||||||
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
|
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
_$DownloaderConfigFromJson(json);
|
_$DownloaderConfigFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$DownloaderConfigToJson(this);
|
Map<String, dynamic> toJson() => _$DownloaderConfigToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class ProtocolConfig {
|
|||||||
|
|
||||||
factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
|
factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
|
||||||
json == null ? ProtocolConfig() : _$ProtocolConfigFromJson(json);
|
json == null ? ProtocolConfig() : _$ProtocolConfigFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this);
|
Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,45 +41,65 @@ class HttpConfig {
|
|||||||
bool useServerCtime;
|
bool useServerCtime;
|
||||||
|
|
||||||
HttpConfig({
|
HttpConfig({
|
||||||
this.userAgent =
|
this.userAgent = '',
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
this.connections = 0,
|
||||||
this.connections = 16,
|
|
||||||
this.useServerCtime = false,
|
this.useServerCtime = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
|
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
_$HttpConfigFromJson(json);
|
_$HttpConfigFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$HttpConfigToJson(this);
|
Map<String, dynamic> toJson() => _$HttpConfigToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class BtConfig {
|
class BtConfig {
|
||||||
int listenPort = 0;
|
int listenPort;
|
||||||
List<String> trackers = [];
|
List<String> trackers;
|
||||||
|
bool seedKeep;
|
||||||
|
double seedRatio;
|
||||||
|
int seedTime;
|
||||||
|
|
||||||
BtConfig();
|
BtConfig({
|
||||||
|
this.listenPort = 0,
|
||||||
|
this.trackers = const [],
|
||||||
|
this.seedKeep = false,
|
||||||
|
this.seedRatio = 0,
|
||||||
|
this.seedTime = 0,
|
||||||
|
});
|
||||||
|
|
||||||
factory BtConfig.fromJson(Map<String, dynamic> json) =>
|
factory BtConfig.fromJson(Map<String, dynamic> json) =>
|
||||||
_$BtConfigFromJson(json);
|
_$BtConfigFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$BtConfigToJson(this);
|
Map<String, dynamic> toJson() => _$BtConfigToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
@JsonSerializable(explicitToJson: true)
|
||||||
class ExtraConfig {
|
class ExtraConfig {
|
||||||
String themeMode = '';
|
String themeMode;
|
||||||
String locale = '';
|
String locale;
|
||||||
|
bool lastDeleteTaskKeep;
|
||||||
|
bool defaultDirectDownload;
|
||||||
|
|
||||||
ExtraConfigBt bt = ExtraConfigBt();
|
ExtraConfigBt bt = ExtraConfigBt();
|
||||||
|
|
||||||
ExtraConfig();
|
ExtraConfig({
|
||||||
|
this.themeMode = '',
|
||||||
|
this.locale = '',
|
||||||
|
this.lastDeleteTaskKeep = false,
|
||||||
|
this.defaultDirectDownload = false,
|
||||||
|
});
|
||||||
|
|
||||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||||
json == null ? ExtraConfig() : _$ExtraConfigFromJson(json);
|
json == null ? ExtraConfig() : _$ExtraConfigFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
|
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class ProxyConfig {
|
class ProxyConfig {
|
||||||
bool enable;
|
bool enable;
|
||||||
|
bool system;
|
||||||
String scheme;
|
String scheme;
|
||||||
String host;
|
String host;
|
||||||
String usr;
|
String usr;
|
||||||
@@ -85,6 +107,7 @@ class ProxyConfig {
|
|||||||
|
|
||||||
ProxyConfig({
|
ProxyConfig({
|
||||||
this.enable = false,
|
this.enable = false,
|
||||||
|
this.system = false,
|
||||||
this.scheme = '',
|
this.scheme = '',
|
||||||
this.host = '',
|
this.host = '',
|
||||||
this.usr = '',
|
this.usr = '',
|
||||||
@@ -110,5 +133,6 @@ class ExtraConfigBt {
|
|||||||
|
|
||||||
factory ExtraConfigBt.fromJson(Map<String, dynamic> json) =>
|
factory ExtraConfigBt.fromJson(Map<String, dynamic> json) =>
|
||||||
_$ExtraConfigBtFromJson(json);
|
_$ExtraConfigBtFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ExtraConfigBtToJson(this);
|
Map<String, dynamic> toJson() => _$ExtraConfigBtToJson(this);
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ part of 'downloader_config.dart';
|
|||||||
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||||
DownloaderConfig(
|
DownloaderConfig(
|
||||||
downloadDir: json['downloadDir'] as String? ?? '',
|
downloadDir: json['downloadDir'] as String? ?? '',
|
||||||
maxRunning: json['maxRunning'] as int? ?? 0,
|
maxRunning: (json['maxRunning'] as num?)?.toInt() ?? 0,
|
||||||
)
|
)
|
||||||
..protocolConfig = ProtocolConfig.fromJson(
|
..protocolConfig = ProtocolConfig.fromJson(
|
||||||
json['protocolConfig'] as Map<String, dynamic>?)
|
json['protocolConfig'] as Map<String, dynamic>?)
|
||||||
@@ -37,9 +37,8 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||||
userAgent: json['userAgent'] as String? ??
|
userAgent: json['userAgent'] as String? ?? '',
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
connections: (json['connections'] as num?)?.toInt() ?? 0,
|
||||||
connections: json['connections'] as int? ?? 16,
|
|
||||||
useServerCtime: json['useServerCtime'] as bool? ?? false,
|
useServerCtime: json['useServerCtime'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,30 +49,44 @@ Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
|
|||||||
'useServerCtime': instance.useServerCtime,
|
'useServerCtime': instance.useServerCtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
|
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig(
|
||||||
..listenPort = json['listenPort'] as int
|
listenPort: (json['listenPort'] as num?)?.toInt() ?? 0,
|
||||||
..trackers =
|
trackers: (json['trackers'] as List<dynamic>?)
|
||||||
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
seedKeep: json['seedKeep'] as bool? ?? false,
|
||||||
|
seedRatio: (json['seedRatio'] as num?)?.toDouble() ?? 0,
|
||||||
|
seedTime: (json['seedTime'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
||||||
'listenPort': instance.listenPort,
|
'listenPort': instance.listenPort,
|
||||||
'trackers': instance.trackers,
|
'trackers': instance.trackers,
|
||||||
|
'seedKeep': instance.seedKeep,
|
||||||
|
'seedRatio': instance.seedRatio,
|
||||||
|
'seedTime': instance.seedTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig()
|
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||||
..themeMode = json['themeMode'] as String
|
themeMode: json['themeMode'] as String? ?? '',
|
||||||
..locale = json['locale'] as String
|
locale: json['locale'] as String? ?? '',
|
||||||
..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
||||||
|
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
||||||
|
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||||
|
|
||||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'themeMode': instance.themeMode,
|
'themeMode': instance.themeMode,
|
||||||
'locale': instance.locale,
|
'locale': instance.locale,
|
||||||
|
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||||
|
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||||
'bt': instance.bt.toJson(),
|
'bt': instance.bt.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
||||||
enable: json['enable'] as bool? ?? false,
|
enable: json['enable'] as bool? ?? false,
|
||||||
|
system: json['system'] as bool? ?? false,
|
||||||
scheme: json['scheme'] as String? ?? '',
|
scheme: json['scheme'] as String? ?? '',
|
||||||
host: json['host'] as String? ?? '',
|
host: json['host'] as String? ?? '',
|
||||||
usr: json['usr'] as String? ?? '',
|
usr: json['usr'] as String? ?? '',
|
||||||
@@ -83,6 +96,7 @@ ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
|||||||
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
|
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'enable': instance.enable,
|
'enable': instance.enable,
|
||||||
|
'system': instance.system,
|
||||||
'scheme': instance.scheme,
|
'scheme': instance.scheme,
|
||||||
'host': instance.host,
|
'host': instance.host,
|
||||||
'usr': instance.usr,
|
'usr': instance.usr,
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ class Request {
|
|||||||
String url;
|
String url;
|
||||||
Object? extra;
|
Object? extra;
|
||||||
Map<String, String>? labels = {};
|
Map<String, String>? labels = {};
|
||||||
|
RequestProxy? proxy;
|
||||||
|
bool skipVerifyCert = false;
|
||||||
|
|
||||||
Request({
|
Request({
|
||||||
required this.url,
|
required this.url,
|
||||||
this.extra,
|
this.extra,
|
||||||
this.labels,
|
this.labels,
|
||||||
|
this.proxy,
|
||||||
|
this.skipVerifyCert = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -45,3 +49,24 @@ class ReqExtraBt {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
|
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RequestProxyMode {
|
||||||
|
follow,
|
||||||
|
none,
|
||||||
|
custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class RequestProxy {
|
||||||
|
RequestProxyMode mode = RequestProxyMode.follow;
|
||||||
|
String scheme = 'http';
|
||||||
|
String host = '';
|
||||||
|
String usr = '';
|
||||||
|
String pwd = '';
|
||||||
|
|
||||||
|
RequestProxy();
|
||||||
|
|
||||||
|
factory RequestProxy.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$RequestProxyFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$RequestProxyToJson(this);
|
||||||
|
}
|
||||||
@@ -12,6 +12,10 @@ Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
|||||||
labels: (json['labels'] as Map<String, dynamic>?)?.map(
|
labels: (json['labels'] as Map<String, dynamic>?)?.map(
|
||||||
(k, e) => MapEntry(k, e as String),
|
(k, e) => MapEntry(k, e as String),
|
||||||
),
|
),
|
||||||
|
proxy: json['proxy'] == null
|
||||||
|
? null
|
||||||
|
: RequestProxy.fromJson(json['proxy'] as Map<String, dynamic>),
|
||||||
|
skipVerifyCert: json['skipVerifyCert'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||||
@@ -27,6 +31,8 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
|||||||
|
|
||||||
writeNotNull('extra', instance.extra);
|
writeNotNull('extra', instance.extra);
|
||||||
writeNotNull('labels', instance.labels);
|
writeNotNull('labels', instance.labels);
|
||||||
|
writeNotNull('proxy', instance.proxy?.toJson());
|
||||||
|
val['skipVerifyCert'] = instance.skipVerifyCert;
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,3 +56,25 @@ Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
|
|||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'trackers': instance.trackers,
|
'trackers': instance.trackers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy()
|
||||||
|
..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode'])
|
||||||
|
..scheme = json['scheme'] as String
|
||||||
|
..host = json['host'] as String
|
||||||
|
..usr = json['usr'] as String
|
||||||
|
..pwd = json['pwd'] as String;
|
||||||
|
|
||||||
|
Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'mode': _$RequestProxyModeEnumMap[instance.mode]!,
|
||||||
|
'scheme': instance.scheme,
|
||||||
|
'host': instance.host,
|
||||||
|
'usr': instance.usr,
|
||||||
|
'pwd': instance.pwd,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _$RequestProxyModeEnumMap = {
|
||||||
|
RequestProxyMode.follow: 'follow',
|
||||||
|
RequestProxyMode.none: 'none',
|
||||||
|
RequestProxyMode.custom: 'custom',
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ part of 'resource.dart';
|
|||||||
|
|
||||||
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
||||||
name: json['name'] as String? ?? "",
|
name: json['name'] as String? ?? "",
|
||||||
size: json['size'] as int? ?? 0,
|
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||||
range: json['range'] as bool? ?? false,
|
range: json['range'] as bool? ?? false,
|
||||||
files: (json['files'] as List<dynamic>)
|
files: (json['files'] as List<dynamic>)
|
||||||
.map((e) => FileInfo.fromJson(e as Map<String, dynamic>))
|
.map((e) => FileInfo.fromJson(e as Map<String, dynamic>))
|
||||||
@@ -27,7 +27,7 @@ Map<String, dynamic> _$ResourceToJson(Resource instance) => <String, dynamic>{
|
|||||||
FileInfo _$FileInfoFromJson(Map<String, dynamic> json) => FileInfo(
|
FileInfo _$FileInfoFromJson(Map<String, dynamic> json) => FileInfo(
|
||||||
path: json['path'] as String? ?? "",
|
path: json['path'] as String? ?? "",
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
size: json['size'] as int? ?? 0,
|
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||||
req: json['req'] == null
|
req: json['req'] == null
|
||||||
? null
|
? null
|
||||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||||
|
|||||||
@@ -6,19 +6,26 @@ part 'task.g.dart';
|
|||||||
|
|
||||||
enum Status { ready, running, pause, wait, error, done }
|
enum Status { ready, running, pause, wait, error, done }
|
||||||
|
|
||||||
|
enum Protocol { http, bt }
|
||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
@JsonSerializable(explicitToJson: true)
|
||||||
class Task {
|
class Task {
|
||||||
String id;
|
String id;
|
||||||
|
String name;
|
||||||
|
Protocol? protocol;
|
||||||
Meta meta;
|
Meta meta;
|
||||||
Status status;
|
Status status;
|
||||||
|
bool uploading;
|
||||||
Progress progress;
|
Progress progress;
|
||||||
DateTime createdAt;
|
DateTime createdAt;
|
||||||
DateTime updatedAt;
|
DateTime updatedAt;
|
||||||
|
|
||||||
Task({
|
Task({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.name,
|
||||||
required this.meta,
|
required this.meta,
|
||||||
required this.status,
|
required this.status,
|
||||||
|
required this.uploading,
|
||||||
required this.progress,
|
required this.progress,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
@@ -34,11 +41,15 @@ class Progress {
|
|||||||
int used;
|
int used;
|
||||||
int speed;
|
int speed;
|
||||||
int downloaded;
|
int downloaded;
|
||||||
|
int uploadSpeed;
|
||||||
|
int uploaded;
|
||||||
|
|
||||||
Progress({
|
Progress({
|
||||||
required this.used,
|
required this.used,
|
||||||
required this.speed,
|
required this.speed,
|
||||||
required this.downloaded,
|
required this.downloaded,
|
||||||
|
required this.uploadSpeed,
|
||||||
|
required this.uploaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Progress.fromJson(Map<String, dynamic> json) =>
|
factory Progress.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -8,21 +8,36 @@ part of 'task.dart';
|
|||||||
|
|
||||||
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||||
|
uploading: json['uploading'] as bool,
|
||||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||||
);
|
)..protocol = $enumDecodeNullable(_$ProtocolEnumMap, json['protocol']);
|
||||||
|
|
||||||
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
Map<String, dynamic> _$TaskToJson(Task instance) {
|
||||||
'id': instance.id,
|
final val = <String, dynamic>{
|
||||||
'meta': instance.meta.toJson(),
|
'id': instance.id,
|
||||||
'status': _$StatusEnumMap[instance.status]!,
|
'name': instance.name,
|
||||||
'progress': instance.progress.toJson(),
|
};
|
||||||
'createdAt': instance.createdAt.toIso8601String(),
|
|
||||||
'updatedAt': instance.updatedAt.toIso8601String(),
|
void writeNotNull(String key, dynamic value) {
|
||||||
};
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('protocol', _$ProtocolEnumMap[instance.protocol]);
|
||||||
|
val['meta'] = instance.meta.toJson();
|
||||||
|
val['status'] = _$StatusEnumMap[instance.status]!;
|
||||||
|
val['uploading'] = instance.uploading;
|
||||||
|
val['progress'] = instance.progress.toJson();
|
||||||
|
val['createdAt'] = instance.createdAt.toIso8601String();
|
||||||
|
val['updatedAt'] = instance.updatedAt.toIso8601String();
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
const _$StatusEnumMap = {
|
const _$StatusEnumMap = {
|
||||||
Status.ready: 'ready',
|
Status.ready: 'ready',
|
||||||
@@ -33,14 +48,23 @@ const _$StatusEnumMap = {
|
|||||||
Status.done: 'done',
|
Status.done: 'done',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _$ProtocolEnumMap = {
|
||||||
|
Protocol.http: 'http',
|
||||||
|
Protocol.bt: 'bt',
|
||||||
|
};
|
||||||
|
|
||||||
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
||||||
used: json['used'] as int,
|
used: (json['used'] as num).toInt(),
|
||||||
speed: json['speed'] as int,
|
speed: (json['speed'] as num).toInt(),
|
||||||
downloaded: json['downloaded'] as int,
|
downloaded: (json['downloaded'] as num).toInt(),
|
||||||
|
uploadSpeed: (json['uploadSpeed'] as num).toInt(),
|
||||||
|
uploaded: (json['uploaded'] as num).toInt(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ProgressToJson(Progress instance) => <String, dynamic>{
|
Map<String, dynamic> _$ProgressToJson(Progress instance) => <String, dynamic>{
|
||||||
'used': instance.used,
|
'used': instance.used,
|
||||||
'speed': instance.speed,
|
'speed': instance.speed,
|
||||||
'downloaded': instance.downloaded,
|
'downloaded': instance.downloaded,
|
||||||
|
'uploadSpeed': instance.uploadSpeed,
|
||||||
|
'uploaded': instance.uploaded,
|
||||||
};
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:gopeed/database/entity.dart';
|
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||||
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:uri_to_file/uri_to_file.dart';
|
import 'package:uri_to_file/uri_to_file.dart';
|
||||||
@@ -17,8 +17,11 @@ import 'package:window_manager/window_manager.dart';
|
|||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
import '../../../../api/model/downloader_config.dart';
|
import '../../../../api/model/downloader_config.dart';
|
||||||
import '../../../../core/common/start_config.dart';
|
import '../../../../core/common/start_config.dart';
|
||||||
|
import '../../../../core/libgopeed_boot.dart';
|
||||||
import '../../../../database/database.dart';
|
import '../../../../database/database.dart';
|
||||||
|
import '../../../../database/entity.dart';
|
||||||
import '../../../../i18n/message.dart';
|
import '../../../../i18n/message.dart';
|
||||||
|
import '../../../../main.dart';
|
||||||
import '../../../../util/locale_manager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
import '../../../../util/log_util.dart';
|
import '../../../../util/log_util.dart';
|
||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
@@ -73,6 +76,7 @@ final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
|||||||
class AppController extends GetxController with WindowListener, TrayListener {
|
class AppController extends GetxController with WindowListener, TrayListener {
|
||||||
static StartConfig? _defaultStartConfig;
|
static StartConfig? _defaultStartConfig;
|
||||||
|
|
||||||
|
final autoStartup = false.obs;
|
||||||
final startConfig = StartConfig().obs;
|
final startConfig = StartConfig().obs;
|
||||||
final runningPort = 0.obs;
|
final runningPort = 0.obs;
|
||||||
final downloaderConfig = DownloaderConfig().obs;
|
final downloaderConfig = DownloaderConfig().obs;
|
||||||
@@ -98,12 +102,16 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
_initTrackerUpdate().onError((error, stackTrace) =>
|
_initTrackerUpdate().onError((error, stackTrace) =>
|
||||||
logger.w("initTrackerUpdate error", error, stackTrace));
|
logger.w("initTrackerUpdate error", error, stackTrace));
|
||||||
|
|
||||||
|
_initLaunchAtStartup().onError((error, stackTrace) =>
|
||||||
|
logger.w("initLaunchAtStartup error", error, stackTrace));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_linkSubscription?.cancel();
|
_linkSubscription?.cancel();
|
||||||
trayManager.removeListener(this);
|
trayManager.removeListener(this);
|
||||||
|
LibgopeedBoot.instance.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -114,6 +122,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// According to the system_manager document, make sure to call setState once on the onWindowFocus event.
|
||||||
|
@override
|
||||||
|
void onWindowFocus() {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onTrayIconMouseDown() {
|
void onTrayIconMouseDown() {
|
||||||
windowManager.show();
|
windowManager.show();
|
||||||
@@ -181,10 +195,20 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
} else if (Util.isMacos()) {
|
} else if (Util.isMacos()) {
|
||||||
await trayManager.setIcon('assets/tray_icon/icon_mac.png',
|
await trayManager.setIcon('assets/tray_icon/icon_mac.png',
|
||||||
isTemplate: true);
|
isTemplate: true);
|
||||||
|
} else if (Platform.environment.containsKey('FLATPAK_ID') ||
|
||||||
|
Platform.environment.containsKey('SNAP')) {
|
||||||
|
await trayManager.setIcon('com.gopeed.Gopeed');
|
||||||
} else {
|
} else {
|
||||||
await trayManager.setIcon('assets/tray_icon/icon.png');
|
await trayManager.setIcon('assets/tray_icon/icon.png');
|
||||||
}
|
}
|
||||||
final menu = Menu(items: [
|
final menu = Menu(items: [
|
||||||
|
MenuItem(
|
||||||
|
label: "show".tr,
|
||||||
|
onClick: (menuItem) async => {
|
||||||
|
await windowManager.show(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItem.separator(),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: "create".tr,
|
label: "create".tr,
|
||||||
onClick: (menuItem) async => {
|
onClick: (menuItem) async => {
|
||||||
@@ -192,14 +216,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE),
|
await Get.rootDelegate.offAndToNamed(Routes.CREATE),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuItem.separator(),
|
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: "startAll".tr,
|
label: "startAll".tr,
|
||||||
onClick: (menuItem) async => {continueAllTasks()},
|
onClick: (menuItem) async => {continueAllTasks(null)},
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: "pauseAll".tr,
|
label: "pauseAll".tr,
|
||||||
onClick: (menuItem) async => {pauseAllTasks()},
|
onClick: (menuItem) async => {pauseAllTasks(null)},
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'setting'.tr,
|
label: 'setting'.tr,
|
||||||
@@ -222,7 +245,14 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
MenuItem.separator(),
|
MenuItem.separator(),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'exit'.tr,
|
label: 'exit'.tr,
|
||||||
onClick: (menuItem) => {windowManager.destroy()},
|
onClick: (menuItem) async {
|
||||||
|
try {
|
||||||
|
await LibgopeedBoot.instance.stop();
|
||||||
|
} catch (e) {
|
||||||
|
logger.w("libgopeed stop fail", e);
|
||||||
|
}
|
||||||
|
windowManager.destroy();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
if (!Util.isLinux()) {
|
if (!Util.isLinux()) {
|
||||||
@@ -240,16 +270,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
FlutterForegroundTask.init(
|
FlutterForegroundTask.init(
|
||||||
androidNotificationOptions: AndroidNotificationOptions(
|
androidNotificationOptions: AndroidNotificationOptions(
|
||||||
channelId: 'gopeed_service',
|
channelId: 'gopeed_service',
|
||||||
channelName: 'Gopeed Background Service',
|
channelName: 'Gopeed Background Service',
|
||||||
channelImportance: NotificationChannelImportance.LOW,
|
channelImportance: NotificationChannelImportance.LOW,
|
||||||
showWhen: true,
|
showWhen: true,
|
||||||
priority: NotificationPriority.LOW,
|
priority: NotificationPriority.LOW,
|
||||||
iconData: const NotificationIconData(
|
),
|
||||||
resType: ResourceType.mipmap,
|
|
||||||
resPrefix: ResourcePrefix.ic,
|
|
||||||
name: 'launcher',
|
|
||||||
)),
|
|
||||||
iosNotificationOptions: const IOSNotificationOptions(
|
iosNotificationOptions: const IOSNotificationOptions(
|
||||||
showNotification: true,
|
showNotification: true,
|
||||||
playSound: false,
|
playSound: false,
|
||||||
@@ -269,12 +295,19 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
FlutterForegroundTask.startService(
|
FlutterForegroundTask.startService(
|
||||||
notificationTitle: "serviceTitle".tr,
|
notificationTitle: "serviceTitle".tr,
|
||||||
notificationText: "serviceText".tr,
|
notificationText: "serviceText".tr,
|
||||||
|
notificationIcon: const NotificationIconData(
|
||||||
|
resType: ResourceType.mipmap,
|
||||||
|
resPrefix: ResourcePrefix.ic,
|
||||||
|
name: 'launcher',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _toCreate(Uri uri) async {
|
Future<void> _toCreate(Uri uri) async {
|
||||||
final path = (uri.scheme == "magnet" || uri.scheme == "http" || uri.scheme == "https")
|
final path = (uri.scheme == "magnet" ||
|
||||||
|
uri.scheme == "http" ||
|
||||||
|
uri.scheme == "https")
|
||||||
? uri.toString()
|
? uri.toString()
|
||||||
: (await toFile(uri.toString())).path;
|
: (await toFile(uri.toString())).path;
|
||||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
|
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
|
||||||
@@ -305,22 +338,24 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
return _defaultStartConfig!;
|
return _defaultStartConfig!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadStartConfig() async {
|
Future<StartConfig> loadStartConfig() async {
|
||||||
final defaultCfg = await _initDefaultStartConfig();
|
final defaultCfg = await _initDefaultStartConfig();
|
||||||
final saveCfg = Database.instance.getStartConfig();
|
final saveCfg = Database.instance.getStartConfig();
|
||||||
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
|
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
|
||||||
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
|
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
|
||||||
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
|
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
|
||||||
|
return startConfig.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadDownloaderConfig() async {
|
Future<DownloaderConfig> loadDownloaderConfig() async {
|
||||||
try {
|
try {
|
||||||
downloaderConfig.value = await getConfig();
|
downloaderConfig.value = await getConfig();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.w("load downloader config fail", e);
|
logger.w("load downloader config fail", e, StackTrace.current);
|
||||||
downloaderConfig.value = DownloaderConfig();
|
downloaderConfig.value = DownloaderConfig();
|
||||||
}
|
}
|
||||||
await _initDownloaderConfig();
|
await _initDownloaderConfig();
|
||||||
|
return downloaderConfig.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> trackerUpdate() async {
|
Future<void> trackerUpdate() async {
|
||||||
@@ -413,12 +448,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
if (Util.isDesktop()) {
|
if (Util.isDesktop()) {
|
||||||
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
|
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
|
||||||
} else if (Util.isAndroid()) {
|
} else if (Util.isAndroid()) {
|
||||||
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
|
config.downloadDir = (await getExternalStorageDirectory())?.path ??
|
||||||
if (downloadDir != null) {
|
(await getApplicationDocumentsDirectory()).path;
|
||||||
config.downloadDir = '$downloadDir/Gopeed';
|
|
||||||
} else {
|
|
||||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
|
||||||
}
|
|
||||||
} else if (Util.isIOS()) {
|
} else if (Util.isIOS()) {
|
||||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||||
} else {
|
} else {
|
||||||
@@ -427,6 +458,17 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initLaunchAtStartup() async {
|
||||||
|
if (!Util.isWindows() && !Util.isLinux()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
launchAtStartup.setup(
|
||||||
|
appName: packageInfo.appName,
|
||||||
|
appPath: Platform.resolvedExecutable,
|
||||||
|
args: ['--${Args.flagHidden}']);
|
||||||
|
autoStartup.value = await launchAtStartup.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveConfig() async {
|
Future<void> saveConfig() async {
|
||||||
Database.instance.saveStartConfig(StartConfigEntity(
|
Database.instance.saveStartConfig(StartConfigEntity(
|
||||||
network: startConfig.value.network,
|
network: startConfig.value.network,
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gopeed/api/model/request.dart';
|
||||||
|
|
||||||
|
import '../../app/controllers/app_controller.dart';
|
||||||
|
|
||||||
class CreateController extends GetxController
|
class CreateController extends GetxController
|
||||||
with GetSingleTickerProviderStateMixin {
|
with GetSingleTickerProviderStateMixin {
|
||||||
@@ -13,6 +16,7 @@ class CreateController extends GetxController
|
|||||||
final isConfirming = false.obs;
|
final isConfirming = false.obs;
|
||||||
final showAdvanced = false.obs;
|
final showAdvanced = false.obs;
|
||||||
final directDownload = false.obs;
|
final directDownload = false.obs;
|
||||||
|
final proxyConfig = Rx<RequestProxy?>(null);
|
||||||
late TabController advancedTabController;
|
late TabController advancedTabController;
|
||||||
final oldUrl = "".obs;
|
final oldUrl = "".obs;
|
||||||
final fileDataUri = "".obs;
|
final fileDataUri = "".obs;
|
||||||
@@ -21,6 +25,11 @@ class CreateController extends GetxController
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
advancedTabController = TabController(length: 2, vsync: this);
|
advancedTabController = TabController(length: 2, vsync: this);
|
||||||
|
directDownload.value = Get.find<AppController>()
|
||||||
|
.downloaderConfig
|
||||||
|
.value
|
||||||
|
.extra
|
||||||
|
.defaultDirectDownload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
||||||
|
import 'package:checkable_treeview/checkable_treeview.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -16,9 +17,11 @@ import '../../../../database/database.dart';
|
|||||||
import '../../../../util/input_formatter.dart';
|
import '../../../../util/input_formatter.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
|
import '../../../../util/extensions.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
|
import '../../../views/compact_checkbox.dart';
|
||||||
import '../../../views/directory_selector.dart';
|
import '../../../views/directory_selector.dart';
|
||||||
import '../../../views/file_list_view.dart';
|
import '../../../views/file_tree_view.dart';
|
||||||
import '../../app/controllers/app_controller.dart';
|
import '../../app/controllers/app_controller.dart';
|
||||||
import '../../history/views/history_view.dart';
|
import '../../history/views/history_view.dart';
|
||||||
import '../controllers/create_controller.dart';
|
import '../controllers/create_controller.dart';
|
||||||
@@ -31,13 +34,20 @@ class CreateView extends GetView<CreateController> {
|
|||||||
final _connectionsController = TextEditingController();
|
final _connectionsController = TextEditingController();
|
||||||
final _pathController = TextEditingController();
|
final _pathController = TextEditingController();
|
||||||
final _confirmController = RoundedLoadingButtonController();
|
final _confirmController = RoundedLoadingButtonController();
|
||||||
|
final _proxyIpController = TextEditingController();
|
||||||
|
final _proxyPortController = TextEditingController();
|
||||||
|
final _proxyUsrController = TextEditingController();
|
||||||
|
final _proxyPwdController = TextEditingController();
|
||||||
final _httpUaController = TextEditingController();
|
final _httpUaController = TextEditingController();
|
||||||
final _httpCookieController = TextEditingController();
|
final _httpCookieController = TextEditingController();
|
||||||
final _httpRefererController = TextEditingController();
|
final _httpRefererController = TextEditingController();
|
||||||
final _btTrackerController = TextEditingController();
|
final _btTrackerController = TextEditingController();
|
||||||
|
final _fileTreeViewKey = GlobalKey<TreeViewState<int>>();
|
||||||
|
|
||||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||||
|
|
||||||
|
final _skipVerifyCertController = false.obs;
|
||||||
|
|
||||||
CreateView({Key? key}) : super(key: key);
|
CreateView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -205,7 +215,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.background,
|
.surface,
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(
|
BorderRadius.circular(
|
||||||
10.0),
|
10.0),
|
||||||
@@ -260,7 +270,164 @@ class CreateView extends GetView<CreateController> {
|
|||||||
() => Visibility(
|
() => Visibility(
|
||||||
visible: controller.showAdvanced.value,
|
visible: controller.showAdvanced.value,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Transform.translate(
|
||||||
|
offset: const Offset(-40, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.wifi_2_bar,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 15,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 150,
|
||||||
|
child: DropdownButton<
|
||||||
|
RequestProxyMode>(
|
||||||
|
hint: Text('proxy'.tr),
|
||||||
|
isExpanded: true,
|
||||||
|
value: controller
|
||||||
|
.proxyConfig.value?.mode,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value != null) {
|
||||||
|
controller.proxyConfig
|
||||||
|
.value = RequestProxy()
|
||||||
|
..mode = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem<
|
||||||
|
RequestProxyMode>(
|
||||||
|
value:
|
||||||
|
RequestProxyMode.follow,
|
||||||
|
child: Text(
|
||||||
|
'followSettings'.tr),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<
|
||||||
|
RequestProxyMode>(
|
||||||
|
value:
|
||||||
|
RequestProxyMode.none,
|
||||||
|
child: Text('noProxy'.tr),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<
|
||||||
|
RequestProxyMode>(
|
||||||
|
value:
|
||||||
|
RequestProxyMode.custom,
|
||||||
|
child:
|
||||||
|
Text('customProxy'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...(controller.proxyConfig.value?.mode ==
|
||||||
|
RequestProxyMode.custom
|
||||||
|
? [
|
||||||
|
SizedBox(
|
||||||
|
width: 150,
|
||||||
|
child: DropdownButtonFormField<
|
||||||
|
String>(
|
||||||
|
value: controller
|
||||||
|
.proxyConfig.value?.scheme,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value != null) {}
|
||||||
|
},
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: 'http',
|
||||||
|
child: Text('HTTP'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: 'https',
|
||||||
|
child: Text('HTTPS'),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: 'socks5',
|
||||||
|
child: Text('SOCKS5'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(children: [
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyIpController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'server'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 10)),
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyPortController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'port'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
keyboardType:
|
||||||
|
TextInputType.number,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter
|
||||||
|
.digitsOnly,
|
||||||
|
NumericalRangeFormatter(
|
||||||
|
min: 0, max: 65535),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Row(children: [
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyUsrController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'username'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: 10)),
|
||||||
|
Flexible(
|
||||||
|
child: TextFormField(
|
||||||
|
controller:
|
||||||
|
_proxyPwdController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'password'.tr,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.all(
|
||||||
|
0.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
]
|
||||||
|
: const []),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
TabBar(
|
TabBar(
|
||||||
controller: controller.advancedTabController,
|
controller: controller.advancedTabController,
|
||||||
tabs: const [
|
tabs: const [
|
||||||
@@ -292,6 +459,22 @@ class CreateView extends GetView<CreateController> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Referer',
|
labelText: 'Referer',
|
||||||
)),
|
)),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(top: 10),
|
||||||
|
child: CompactCheckbox(
|
||||||
|
label: 'skipVerifyCert'.tr,
|
||||||
|
value:
|
||||||
|
_skipVerifyCertController.value,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
_skipVerifyCertController.value =
|
||||||
|
value ?? false;
|
||||||
|
},
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
@@ -300,7 +483,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
controller: _btTrackerController,
|
controller: _btTrackerController,
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Trakers',
|
labelText: 'Trackers',
|
||||||
hintText: 'addTrackerHit'.tr,
|
hintText: 'addTrackerHit'.tr,
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
@@ -321,23 +504,13 @@ class CreateView extends GetView<CreateController> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
CompactCheckbox(
|
||||||
onPressed: () {
|
label: 'directDownload'.tr,
|
||||||
controller.directDownload.value =
|
value: controller.directDownload.value,
|
||||||
!controller.directDownload.value;
|
onChanged: (bool? value) {
|
||||||
},
|
controller.directDownload.value =
|
||||||
child: Row(children: [
|
value ?? false;
|
||||||
Obx(() => Checkbox(
|
}),
|
||||||
value:
|
|
||||||
controller.directDownload.value,
|
|
||||||
onChanged: (bool? value) {
|
|
||||||
controller.directDownload.value =
|
|
||||||
value ?? false;
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
Text('directDownload'.tr),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.showAdvanced.value =
|
controller.showAdvanced.value =
|
||||||
@@ -405,7 +578,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
Util.isWeb() && controller.fileDataUri.isNotEmpty;
|
Util.isWeb() && controller.fileDataUri.isNotEmpty;
|
||||||
final submitUrl = isWebFileChosen
|
final submitUrl = isWebFileChosen
|
||||||
? controller.fileDataUri.value
|
? controller.fileDataUri.value
|
||||||
: _urlController.text;
|
: _urlController.text.trim();
|
||||||
|
|
||||||
final urls = Util.textToLines(submitUrl);
|
final urls = Util.textToLines(submitUrl);
|
||||||
// Add url to the history
|
// Add url to the history
|
||||||
@@ -415,7 +588,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Check if is direct download, there has two ways to direct download
|
Check if is direct download, there has two ways to direct download
|
||||||
1. Direct download option is checked
|
1. Direct download option is checked
|
||||||
2. Muli line urls
|
2. Muli line urls
|
||||||
@@ -425,18 +598,26 @@ class CreateView extends GetView<CreateController> {
|
|||||||
if (isDirect) {
|
if (isDirect) {
|
||||||
await Future.wait(urls.map((url) {
|
await Future.wait(urls.map((url) {
|
||||||
return createTask(CreateTask(
|
return createTask(CreateTask(
|
||||||
req: Request(url: url, extra: parseReqExtra(url)),
|
req: Request(
|
||||||
|
url: url,
|
||||||
|
extra: parseReqExtra(url),
|
||||||
|
proxy: parseProxy(),
|
||||||
|
skipVerifyCert: _skipVerifyCertController.value,
|
||||||
|
),
|
||||||
opt: Options(
|
opt: Options(
|
||||||
name: isMultiLine ? "" : _renameController.text,
|
name: isMultiLine ? "" : _renameController.text,
|
||||||
path: _pathController.text,
|
path: _pathController.text,
|
||||||
selectFiles: [],
|
selectFiles: [],
|
||||||
extra: parseReqOptsExtra())));
|
extra: parseReqOptsExtra(),
|
||||||
|
)));
|
||||||
}));
|
}));
|
||||||
Get.rootDelegate.offNamed(Routes.TASK);
|
Get.rootDelegate.offNamed(Routes.TASK);
|
||||||
} else {
|
} else {
|
||||||
final rr = await resolve(Request(
|
final rr = await resolve(Request(
|
||||||
url: submitUrl,
|
url: submitUrl,
|
||||||
extra: parseReqExtra(_urlController.text),
|
extra: parseReqExtra(_urlController.text),
|
||||||
|
proxy: parseProxy(),
|
||||||
|
skipVerifyCert: _skipVerifyCertController.value,
|
||||||
));
|
));
|
||||||
await _showResolveDialog(rr);
|
await _showResolveDialog(rr);
|
||||||
}
|
}
|
||||||
@@ -449,20 +630,39 @@ class CreateView extends GetView<CreateController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RequestProxy? parseProxy() {
|
||||||
|
if (controller.proxyConfig.value?.mode == RequestProxyMode.custom) {
|
||||||
|
return RequestProxy()
|
||||||
|
..mode = RequestProxyMode.custom
|
||||||
|
..scheme = _proxyIpController.text
|
||||||
|
..host = "${_proxyIpController.text}:${_proxyPortController.text}"
|
||||||
|
..usr = _proxyUsrController.text
|
||||||
|
..pwd = _proxyPwdController.text;
|
||||||
|
}
|
||||||
|
return controller.proxyConfig.value;
|
||||||
|
}
|
||||||
|
|
||||||
Object? parseReqExtra(String url) {
|
Object? parseReqExtra(String url) {
|
||||||
Object? reqExtra;
|
Object? reqExtra;
|
||||||
if (controller.showAdvanced.value) {
|
if (controller.showAdvanced.value) {
|
||||||
final u = Uri.parse(_urlController.text);
|
switch (controller.advancedTabController.index) {
|
||||||
if (u.scheme.startsWith("http")) {
|
case 0:
|
||||||
reqExtra = ReqExtraHttp()
|
final header = {
|
||||||
..header = {
|
|
||||||
"User-Agent": _httpUaController.text,
|
"User-Agent": _httpUaController.text,
|
||||||
"Cookie": _httpCookieController.text,
|
"Cookie": _httpCookieController.text,
|
||||||
"Referer": _httpRefererController.text,
|
"Referer": _httpRefererController.text,
|
||||||
};
|
};
|
||||||
} else {
|
header.removeWhere((key, value) => value.trim().isEmpty);
|
||||||
reqExtra = ReqExtraBt()
|
if (header.isNotEmpty) {
|
||||||
..trackers = Util.textToLines(_btTrackerController.text);
|
reqExtra = ReqExtraHttp()..header = header;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (_btTrackerController.text.trim().isNotEmpty) {
|
||||||
|
reqExtra = ReqExtraBt()
|
||||||
|
..trackers = Util.textToLines(_btTrackerController.text);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reqExtra;
|
return reqExtra;
|
||||||
@@ -501,7 +701,13 @@ class CreateView extends GetView<CreateController> {
|
|||||||
child: Form(
|
child: Form(
|
||||||
key: createFormKey,
|
key: createFormKey,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
child: FileListView(files: rr.res.files)),
|
child: FileTreeView(
|
||||||
|
files: rr.res.files,
|
||||||
|
initialValues: rr.res.files.asMap().keys.toList(),
|
||||||
|
onSelectionChanged: (List<int> values) {
|
||||||
|
controller.selectedIndexes.value = values;
|
||||||
|
},
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -545,7 +751,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
controller.selectedIndexes.map((index) {
|
controller.selectedIndexes.map((index) {
|
||||||
final file = rr.res.files[index];
|
final file = rr.res.files[index];
|
||||||
return createTask(CreateTask(
|
return createTask(CreateTask(
|
||||||
req: file.req!,
|
req: file.req!..proxy = parseProxy(),
|
||||||
opt: Options(
|
opt: Options(
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: path.join(_pathController.text,
|
path: path.join(_pathController.text,
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gopeed/app/views/copy_button.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../../../../api/model/downloader_config.dart';
|
||||||
import '../../../../i18n/message.dart';
|
import '../../../../i18n/message.dart';
|
||||||
import '../../../../util/input_formatter.dart';
|
import '../../../../util/input_formatter.dart';
|
||||||
import '../../../../util/locale_manager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
|
import '../../../../util/log_util.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
@@ -33,13 +38,22 @@ class SettingView extends GetView<SettingController> {
|
|||||||
final startCfg = appController.startConfig;
|
final startCfg = appController.startConfig;
|
||||||
|
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
debounceSave({bool needRestart = false}) {
|
Future<bool> debounceSave(
|
||||||
var completer = Completer<void>();
|
{Future<String> Function()? check, bool needRestart = false}) {
|
||||||
|
var completer = Completer<bool>();
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
timer = Timer(const Duration(milliseconds: 1000), () {
|
timer = Timer(const Duration(milliseconds: 1000), () async {
|
||||||
|
if (check != null) {
|
||||||
|
final checkResult = await check();
|
||||||
|
if (checkResult.isNotEmpty) {
|
||||||
|
showErrorMessage(checkResult);
|
||||||
|
completer.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
appController
|
appController
|
||||||
.saveConfig()
|
.saveConfig()
|
||||||
.then(completer.complete)
|
.then((_) => completer.complete(true))
|
||||||
.onError(completer.completeError);
|
.onError(completer.completeError);
|
||||||
if (needRestart) {
|
if (needRestart) {
|
||||||
showMessage('tip'.tr, 'effectAfterRestart'.tr);
|
showMessage('tip'.tr, 'effectAfterRestart'.tr);
|
||||||
@@ -66,6 +80,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
return DirectorySelector(
|
return DirectorySelector(
|
||||||
controller: downloadDirController,
|
controller: downloadDirController,
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
|
showAndoirdToggle: true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
final buildMaxRunning = _buildConfigItem(
|
final buildMaxRunning = _buildConfigItem(
|
||||||
@@ -94,16 +109,82 @@ class SettingView extends GetView<SettingController> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final buildDefaultDirectDownload =
|
||||||
|
_buildConfigItem('defaultDirectDownload'.tr, () {
|
||||||
|
return appController.downloaderConfig.value.extra.defaultDirectDownload
|
||||||
|
? 'on'.tr
|
||||||
|
: 'off'.tr;
|
||||||
|
}, (Key key) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Switch(
|
||||||
|
value:
|
||||||
|
appController.downloaderConfig.value.extra.defaultDirectDownload,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
appController.downloaderConfig.update((val) {
|
||||||
|
val!.extra.defaultDirectDownload = value;
|
||||||
|
});
|
||||||
|
await debounceSave();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
buildBrowserExtension() {
|
buildBrowserExtension() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text('browserExtension'.tr),
|
title: Text('browserExtension'.tr),
|
||||||
subtitle: const OpenInNew(
|
subtitle: const Row(
|
||||||
text: "Chrome",
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
url:
|
children: [
|
||||||
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
|
OpenInNew(
|
||||||
));
|
text: "Chrome",
|
||||||
|
url:
|
||||||
|
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
OpenInNew(
|
||||||
|
text: "Edge",
|
||||||
|
url:
|
||||||
|
"https://microsoftedge.microsoft.com/addons/detail/dkajnckekendchdleoaenoophcobooce",
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
OpenInNew(
|
||||||
|
text: "Firefox",
|
||||||
|
url:
|
||||||
|
"https://addons.mozilla.org/zh-CN/firefox/addon/gopeed-extension",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(top: 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently auto startup only support Windows and Linux
|
||||||
|
final buildAutoStartup = !Util.isWindows() && !Util.isLinux()
|
||||||
|
? () => null
|
||||||
|
: _buildConfigItem('launchAtStartup'.tr, () {
|
||||||
|
return appController.autoStartup.value ? 'on'.tr : 'off'.tr;
|
||||||
|
}, (Key key) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Switch(
|
||||||
|
value: appController.autoStartup.value,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
try {
|
||||||
|
if (value) {
|
||||||
|
await launchAtStartup.enable();
|
||||||
|
} else {
|
||||||
|
await launchAtStartup.disable();
|
||||||
|
}
|
||||||
|
appController.autoStartup.value = value;
|
||||||
|
} catch (e) {
|
||||||
|
showErrorMessage(e);
|
||||||
|
logger.e('launchAtStartup fail', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// http config items start
|
// http config items start
|
||||||
final httpConfig = downloaderCfg.value.protocolConfig.http;
|
final httpConfig = downloaderCfg.value.protocolConfig.http;
|
||||||
final buildHttpUa =
|
final buildHttpUa =
|
||||||
@@ -276,6 +357,69 @@ class SettingView extends GetView<SettingController> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
final buildBtSeedConfig = _buildConfigItem('seedConfig',
|
||||||
|
() => '${'seedKeep'.tr}(${btConfig.seedKeep ? 'on'.tr : 'off'.tr})',
|
||||||
|
(Key key) {
|
||||||
|
final seedRatioController =
|
||||||
|
TextEditingController(text: btConfig.seedRatio.toString());
|
||||||
|
seedRatioController.addListener(() {
|
||||||
|
if (seedRatioController.text.isNotEmpty) {
|
||||||
|
btConfig.seedRatio = double.parse(seedRatioController.text);
|
||||||
|
debounceSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final seedTimeController =
|
||||||
|
TextEditingController(text: (btConfig.seedTime ~/ 60).toString());
|
||||||
|
seedTimeController.addListener(() {
|
||||||
|
if (seedTimeController.text.isNotEmpty) {
|
||||||
|
btConfig.seedTime = int.parse(seedTimeController.text) * 60;
|
||||||
|
debounceSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SwitchListTile(
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: btConfig.seedKeep,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
downloaderCfg.update((val) {
|
||||||
|
val!.protocolConfig.bt.seedKeep = value;
|
||||||
|
});
|
||||||
|
debounceSave();
|
||||||
|
},
|
||||||
|
title: Text('seedKeep'.tr)),
|
||||||
|
btConfig.seedKeep
|
||||||
|
? null
|
||||||
|
: TextField(
|
||||||
|
controller: seedRatioController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'seedRatio'.tr,
|
||||||
|
),
|
||||||
|
keyboardType:
|
||||||
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(
|
||||||
|
RegExp(r'^\d+\.?\d{0,2}')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
btConfig.seedKeep
|
||||||
|
? null
|
||||||
|
: TextField(
|
||||||
|
controller: seedTimeController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'seedTime'.tr,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
|
NumericalRangeFormatter(min: 0, max: 100000000),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
].where((e) => e != null).map((e) => e!).toList(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// ui config items start
|
// ui config items start
|
||||||
final buildTheme = _buildConfigItem(
|
final buildTheme = _buildConfigItem(
|
||||||
@@ -408,29 +552,53 @@ class SettingView extends GetView<SettingController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// advanced config proxy items start
|
// advanced config proxy items start
|
||||||
|
final proxy = downloaderCfg.value.proxy;
|
||||||
final buildProxy = _buildConfigItem(
|
final buildProxy = _buildConfigItem(
|
||||||
'proxy',
|
'proxy',
|
||||||
() => downloaderCfg.value.proxy.enable
|
() {
|
||||||
? '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}'
|
switch (proxy.proxyMode) {
|
||||||
: 'notSet'.tr,
|
case ProxyModeEnum.noProxy:
|
||||||
|
return 'noProxy'.tr;
|
||||||
|
case ProxyModeEnum.systemProxy:
|
||||||
|
return 'systemProxy'.tr;
|
||||||
|
case ProxyModeEnum.customProxy:
|
||||||
|
return '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}';
|
||||||
|
}
|
||||||
|
},
|
||||||
(Key key) {
|
(Key key) {
|
||||||
final proxy = downloaderCfg.value.proxy;
|
final mode = SizedBox(
|
||||||
|
width: 150,
|
||||||
|
child: DropdownButtonFormField<ProxyModeEnum>(
|
||||||
|
value: proxy.proxyMode,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value != null && value != proxy.proxyMode) {
|
||||||
|
proxy.proxyMode = value;
|
||||||
|
downloaderCfg.update((val) {
|
||||||
|
val!.proxy = proxy;
|
||||||
|
});
|
||||||
|
|
||||||
final switcher = Switch(
|
await debounceSave();
|
||||||
value: proxy.enable,
|
}
|
||||||
onChanged: (bool value) async {
|
},
|
||||||
if (value != proxy.enable) {
|
items: [
|
||||||
downloaderCfg.update((val) {
|
DropdownMenuItem<ProxyModeEnum>(
|
||||||
val!.proxy.enable = value;
|
value: ProxyModeEnum.noProxy,
|
||||||
});
|
child: Text('noProxy'.tr),
|
||||||
|
),
|
||||||
await debounceSave();
|
DropdownMenuItem<ProxyModeEnum>(
|
||||||
}
|
value: ProxyModeEnum.systemProxy,
|
||||||
},
|
child: Text('systemProxy'.tr),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<ProxyModeEnum>(
|
||||||
|
value: ProxyModeEnum.customProxy,
|
||||||
|
child: Text('customProxy'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final scheme = SizedBox(
|
final scheme = SizedBox(
|
||||||
width: 100,
|
width: 150,
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: proxy.scheme,
|
value: proxy.scheme,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
@@ -478,7 +646,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
|
|
||||||
ipController.addListener(updateAddress);
|
ipController.addListener(updateAddress);
|
||||||
portController.addListener(updateAddress);
|
portController.addListener(updateAddress);
|
||||||
final server = [
|
final server = Row(children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: ipController,
|
controller: ipController,
|
||||||
@@ -503,12 +671,12 @@ class SettingView extends GetView<SettingController> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
]);
|
||||||
|
|
||||||
final usrController = TextEditingController(text: proxy.usr);
|
final usrController = TextEditingController(text: proxy.usr);
|
||||||
final pwdController = TextEditingController(text: proxy.pwd);
|
final pwdController = TextEditingController(text: proxy.pwd);
|
||||||
|
|
||||||
final auth = [
|
final auth = Row(children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: usrController,
|
controller: usrController,
|
||||||
@@ -528,20 +696,21 @@ class SettingView extends GetView<SettingController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
]);
|
||||||
|
|
||||||
|
List<Widget> customView() {
|
||||||
|
if (proxy.proxyMode != ProxyModeEnum.customProxy) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [scheme, server, auth];
|
||||||
|
}
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: _addPadding([
|
children: _addPadding([
|
||||||
switcher,
|
mode,
|
||||||
scheme,
|
...customView(),
|
||||||
Row(
|
|
||||||
children: server,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: auth,
|
|
||||||
),
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -596,11 +765,41 @@ class SettingView extends GetView<SettingController> {
|
|||||||
final ipController = TextEditingController(text: ip);
|
final ipController = TextEditingController(text: ip);
|
||||||
final portController = TextEditingController(text: port);
|
final portController = TextEditingController(text: port);
|
||||||
updateAddress() async {
|
updateAddress() async {
|
||||||
|
if (ipController.text.isEmpty || portController.text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final newAddress = '${ipController.text}:${portController.text}';
|
final newAddress = '${ipController.text}:${portController.text}';
|
||||||
if (newAddress != startCfg.value.address) {
|
if (newAddress != startCfg.value.address) {
|
||||||
startCfg.value.address = newAddress;
|
startCfg.value.address = newAddress;
|
||||||
|
|
||||||
await debounceSave(needRestart: true);
|
final saved = await debounceSave(
|
||||||
|
check: () async {
|
||||||
|
// Check if address already in use
|
||||||
|
final configIp = ipController.text;
|
||||||
|
final configPort = int.parse(portController.text);
|
||||||
|
if (configPort == 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final socket = await Socket.connect(configIp, configPort,
|
||||||
|
timeout: const Duration(seconds: 3));
|
||||||
|
socket.close();
|
||||||
|
return 'portInUse'
|
||||||
|
.trParams({'port': configPort.toString()});
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
needRestart: true);
|
||||||
|
|
||||||
|
// If save failed, restore the old address
|
||||||
|
if (!saved) {
|
||||||
|
final oldAddress =
|
||||||
|
(await appController.loadStartConfig()).address;
|
||||||
|
startCfg.update((val) async {
|
||||||
|
val!.address = oldAddress;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +865,32 @@ class SettingView extends GetView<SettingController> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// advanced config log items start
|
||||||
|
buildLogsDir() {
|
||||||
|
return ListTile(
|
||||||
|
title: Text("logDirectory".tr),
|
||||||
|
subtitle: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: TextEditingController(text: logsDir()),
|
||||||
|
enabled: false,
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Util.isDesktop()
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
onPressed: () {
|
||||||
|
launchUrl(Uri.file(logsDir()));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: CopyButton(logsDir()),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -700,7 +925,9 @@ class SettingView extends GetView<SettingController> {
|
|||||||
children: _addDivider([
|
children: _addDivider([
|
||||||
buildDownloadDir(),
|
buildDownloadDir(),
|
||||||
buildMaxRunning(),
|
buildMaxRunning(),
|
||||||
buildBrowserExtension()
|
buildDefaultDirectDownload(),
|
||||||
|
buildBrowserExtension(),
|
||||||
|
buildAutoStartup(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
const Text('HTTP'),
|
const Text('HTTP'),
|
||||||
@@ -719,6 +946,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
buildBtListenPort(),
|
buildBtListenPort(),
|
||||||
buildBtTrackerSubscribeUrls(),
|
buildBtTrackerSubscribeUrls(),
|
||||||
buildBtTrackers(),
|
buildBtTrackers(),
|
||||||
|
buildBtSeedConfig(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
Text('ui'.tr),
|
Text('ui'.tr),
|
||||||
@@ -775,6 +1003,13 @@ class SettingView extends GetView<SettingController> {
|
|||||||
: null,
|
: null,
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
|
Text('developer'.tr),
|
||||||
|
Card(
|
||||||
|
child: Column(
|
||||||
|
children: _addDivider([
|
||||||
|
buildLogsDir(),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
]),
|
]),
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
@@ -857,3 +1092,37 @@ class SettingView extends GetView<SettingController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ProxyModeEnum {
|
||||||
|
noProxy,
|
||||||
|
systemProxy,
|
||||||
|
customProxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProxyMode on ProxyConfig {
|
||||||
|
ProxyModeEnum get proxyMode {
|
||||||
|
if (!enable) {
|
||||||
|
return ProxyModeEnum.noProxy;
|
||||||
|
}
|
||||||
|
if (system) {
|
||||||
|
return ProxyModeEnum.systemProxy;
|
||||||
|
}
|
||||||
|
return ProxyModeEnum.customProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
set proxyMode(ProxyModeEnum value) {
|
||||||
|
switch (value) {
|
||||||
|
case ProxyModeEnum.noProxy:
|
||||||
|
enable = false;
|
||||||
|
break;
|
||||||
|
case ProxyModeEnum.systemProxy:
|
||||||
|
enable = true;
|
||||||
|
system = true;
|
||||||
|
break;
|
||||||
|
case ProxyModeEnum.customProxy:
|
||||||
|
enable = true;
|
||||||
|
system = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gopeed/api/model/task.dart';
|
||||||
|
|
||||||
class TaskController extends GetxController {
|
class TaskController extends GetxController {
|
||||||
final tabIndex = 0.obs;
|
final tabIndex = 0.obs;
|
||||||
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
final selectTask = Rx<Task?>(null);
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,13 @@ class TaskDownloadingController extends TaskListController {
|
|||||||
Status.pause,
|
Status.pause,
|
||||||
Status.wait,
|
Status.wait,
|
||||||
Status.error
|
Status.error
|
||||||
], (a, b) => b.createdAt.compareTo(a.createdAt));
|
], (a, b) {
|
||||||
|
if (a.status == Status.running && b.status != Status.running) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.status != Status.running && b.status == Status.running) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return b.updatedAt.compareTo(a.updatedAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ abstract class TaskListController extends GetxController {
|
|||||||
TaskListController(this.statuses, this.compare);
|
TaskListController(this.statuses, this.compare);
|
||||||
|
|
||||||
final tasks = <Task>[].obs;
|
final tasks = <Task>[].obs;
|
||||||
|
final selectedTaskIds = <String>[].obs;
|
||||||
final isRunning = false.obs;
|
final isRunning = false.obs;
|
||||||
|
|
||||||
late final Timer _timer;
|
late final Timer _timer;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class TaskDownloadedView extends GetView<TaskDownloadedController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BuildTaskListView(tasks: controller.tasks);
|
return BuildTaskListView(
|
||||||
|
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ class TaskDownloadingView extends GetView<TaskDownloadingController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BuildTaskListView(tasks: controller.tasks);
|
return BuildTaskListView(
|
||||||
|
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,9 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
|
|
||||||
import '../../../../api/api.dart' as api;
|
import '../../../../api/api.dart' as api;
|
||||||
import '../../../../util/browser_download/browser_download.dart';
|
import '../../../../util/browser_download/browser_download.dart';
|
||||||
import '../../../../util/file_icon.dart';
|
|
||||||
import '../../../../util/icons.dart';
|
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../views/breadcrumb_view.dart';
|
import '../../../views/breadcrumb_view.dart';
|
||||||
|
import '../../../views/file_icon.dart';
|
||||||
import '../controllers/task_files_controller.dart';
|
import '../controllers/task_files_controller.dart';
|
||||||
|
|
||||||
class TaskFilesView extends GetView<TaskFilesController> {
|
class TaskFilesView extends GetView<TaskFilesController> {
|
||||||
@@ -65,9 +64,8 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
|||||||
[meta.opts.path, meta.res!.name, fileRelativePath]);
|
[meta.opts.path, meta.res!.name, fileRelativePath]);
|
||||||
final fileName = basename(filePath);
|
final fileName = basename(filePath);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: file.isDirectory
|
leading:
|
||||||
? const Icon(Icons.folder)
|
Icon(fileIcon(fileName, isFolder: file.isDirectory)),
|
||||||
: Icon(FaIcons.allIcons[findIcon(fileName)]),
|
|
||||||
title: Text(fileName),
|
title: Text(fileName),
|
||||||
subtitle: file.isDirectory
|
subtitle: file.isDirectory
|
||||||
? Text('items'.trParams({
|
? Text('items'.trParams({
|
||||||
@@ -89,7 +87,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
|||||||
return [
|
return [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon:
|
icon:
|
||||||
const Icon(Icons.play_circle),
|
const Icon(Icons.open_in_new),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launchUrl(Uri.parse(accessUrl),
|
launchUrl(Uri.parse(accessUrl),
|
||||||
webOnlyWindowName:
|
webOnlyWindowName:
|
||||||
@@ -104,14 +102,9 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
|||||||
}()
|
}()
|
||||||
: [
|
: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.play_circle),
|
icon: const Icon(Icons.open_in_new),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
print(filePath);
|
await OpenFilex.open(filePath);
|
||||||
try {
|
|
||||||
await OpenFilex.open(filePath);
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:open_filex/open_filex.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import '../../../../api/model/task.dart';
|
||||||
|
import '../../../../util/file_explorer.dart';
|
||||||
|
import '../../../../util/util.dart';
|
||||||
|
import '../../../routes/app_pages.dart';
|
||||||
|
import '../../../views/copy_button.dart';
|
||||||
import '../controllers/task_controller.dart';
|
import '../controllers/task_controller.dart';
|
||||||
import '../controllers/task_downloaded_controller.dart';
|
import '../controllers/task_downloaded_controller.dart';
|
||||||
import '../controllers/task_downloading_controller.dart';
|
import '../controllers/task_downloading_controller.dart';
|
||||||
@@ -12,9 +19,12 @@ class TaskView extends GetView<TaskController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final selectTask = controller.selectTask;
|
||||||
|
|
||||||
return DefaultTabController(
|
return DefaultTabController(
|
||||||
length: 2,
|
length: 2,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
key: controller.scaffoldKey,
|
||||||
appBar: PreferredSize(
|
appBar: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(56),
|
preferredSize: const Size.fromHeight(56),
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
@@ -54,7 +64,87 @@ class TaskView extends GetView<TaskController> {
|
|||||||
TaskDownloadedView(),
|
TaskDownloadedView(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
endDrawer: Drawer(
|
||||||
|
// Add a ListView to the drawer. This ensures the user can scroll
|
||||||
|
// through the options in the drawer if there isn't enough vertical
|
||||||
|
// space to fit everything.
|
||||||
|
child: Obx(() => ListView(
|
||||||
|
// Important: Remove any padding from the ListView.
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: MediaQuery.of(context).padding.top + 65,
|
||||||
|
child: DrawerHeader(
|
||||||
|
child: Text(
|
||||||
|
'taskDetail'.tr,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('taskName'.tr),
|
||||||
|
subtitle: buildTooltipSubtitle(selectTask.value?.name)),
|
||||||
|
ListTile(
|
||||||
|
title: Text('taskUrl'.tr),
|
||||||
|
subtitle:
|
||||||
|
buildTooltipSubtitle(selectTask.value?.meta.req.url),
|
||||||
|
trailing: CopyButton(selectTask.value?.meta.req.url),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('downloadPath'.tr),
|
||||||
|
subtitle:
|
||||||
|
buildTooltipSubtitle(selectTask.value?.explorerUrl),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
onPressed: () {
|
||||||
|
selectTask.value?.explorer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTooltipSubtitle(String? text) {
|
||||||
|
final showText = text ?? "";
|
||||||
|
return Tooltip(
|
||||||
|
message: showText,
|
||||||
|
child: Text(
|
||||||
|
showText,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TaskEnhance on Task {
|
||||||
|
bool get isFolder {
|
||||||
|
return meta.res?.name.isNotEmpty ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get explorerUrl {
|
||||||
|
return path.join(Util.safeDir(meta.opts.path), Util.safeDir(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> explorer() async {
|
||||||
|
if (Util.isDesktop()) {
|
||||||
|
await FileExplorer.openAndSelectFile(explorerUrl);
|
||||||
|
} else {
|
||||||
|
Get.rootDelegate.toNamed(Routes.TASK_FILES, parameters: {'id': id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> open() async {
|
||||||
|
if (status != Status.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFolder) {
|
||||||
|
await explorer();
|
||||||
|
} else {
|
||||||
|
await OpenFilex.open(explorerUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
|
import 'package:contextmenu/contextmenu.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
import '../../api/api.dart';
|
import '../../api/api.dart';
|
||||||
import '../../api/model/meta.dart';
|
|
||||||
import '../../api/model/task.dart';
|
import '../../api/model/task.dart';
|
||||||
import '../../util/file_explorer.dart';
|
|
||||||
import '../../util/file_icon.dart';
|
|
||||||
import '../../util/icons.dart';
|
|
||||||
import '../../util/message.dart';
|
import '../../util/message.dart';
|
||||||
import '../../util/util.dart';
|
import '../../util/util.dart';
|
||||||
|
import '../modules/app/controllers/app_controller.dart';
|
||||||
|
import '../modules/task/controllers/task_controller.dart';
|
||||||
|
import '../modules/task/controllers/task_downloaded_controller.dart';
|
||||||
|
import '../modules/task/controllers/task_downloading_controller.dart';
|
||||||
|
import '../modules/task/views/task_view.dart';
|
||||||
import '../routes/app_pages.dart';
|
import '../routes/app_pages.dart';
|
||||||
|
import 'file_icon.dart';
|
||||||
|
|
||||||
class BuildTaskListView extends GetView {
|
class BuildTaskListView extends GetView {
|
||||||
final List<Task> tasks;
|
final List<Task> tasks;
|
||||||
|
final List<String> selectedTaskIds;
|
||||||
|
|
||||||
const BuildTaskListView({
|
const BuildTaskListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.tasks,
|
required this.tasks,
|
||||||
|
required this.selectedTaskIds,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -57,18 +61,16 @@ class BuildTaskListView extends GetView {
|
|||||||
return task.status == Status.running;
|
return task.status == Status.running;
|
||||||
}
|
}
|
||||||
|
|
||||||
String buildExplorerUrl(Task task) {
|
bool isSelect() {
|
||||||
if (task.meta.res!.name.isEmpty) {
|
return selectedTaskIds.contains(task.id);
|
||||||
return path.join(Util.safeDir(task.meta.opts.path),
|
|
||||||
Util.safeDir(task.meta.res!.files[0].path), fileName(task.meta));
|
|
||||||
} else {
|
|
||||||
return path.join(Util.safeDir(task.meta.opts.path),
|
|
||||||
Util.safeDir(fileName(task.meta)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showDeleteDialog(String id) {
|
bool isFolderTask() {
|
||||||
final keep = true.obs;
|
return task.isFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showDeleteDialog(List<String> ids) {
|
||||||
|
final appController = Get.find<AppController>();
|
||||||
|
|
||||||
final context = Get.context!;
|
final context = Get.context!;
|
||||||
|
|
||||||
@@ -76,13 +78,17 @@ class BuildTaskListView extends GetView {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => AlertDialog(
|
builder: (_) => AlertDialog(
|
||||||
title: Text('deleteTask'.tr),
|
title: Text(
|
||||||
|
'deleteTask'.trParams({'count': ids.length.toString()})),
|
||||||
content: Obx(() => CheckboxListTile(
|
content: Obx(() => CheckboxListTile(
|
||||||
value: keep.value,
|
value: appController
|
||||||
|
.downloaderConfig.value.extra.lastDeleteTaskKeep,
|
||||||
title: Text('deleteTaskTip'.tr,
|
title: Text('deleteTaskTip'.tr,
|
||||||
style: context.textTheme.bodyLarge),
|
style: context.textTheme.bodyLarge),
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
keep.value = v!;
|
appController.downloaderConfig.update((val) {
|
||||||
|
val!.extra.lastDeleteTaskKeep = v!;
|
||||||
|
});
|
||||||
})),
|
})),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -96,7 +102,10 @@ class BuildTaskListView extends GetView {
|
|||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
await deleteTask(id, !keep.value);
|
final force = !appController
|
||||||
|
.downloaderConfig.value.extra.lastDeleteTaskKeep;
|
||||||
|
await appController.saveConfig();
|
||||||
|
await deleteTasks(ids, force);
|
||||||
Get.back();
|
Get.back();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorMessage(e);
|
showErrorMessage(e);
|
||||||
@@ -113,12 +122,7 @@ class BuildTaskListView extends GetView {
|
|||||||
list.add(IconButton(
|
list.add(IconButton(
|
||||||
icon: const Icon(Icons.folder_open),
|
icon: const Icon(Icons.folder_open),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (Util.isDesktop()) {
|
task.explorer();
|
||||||
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
|
|
||||||
} else {
|
|
||||||
Get.rootDelegate
|
|
||||||
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
@@ -149,12 +153,35 @@ class BuildTaskListView extends GetView {
|
|||||||
list.add(IconButton(
|
list.add(IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDeleteDialog(task.id);
|
showDeleteDialog([task.id]);
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildContextItem(IconData icon, String label, Function() onTap,
|
||||||
|
{bool enabled = true}) {
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
visualDensity: const VisualDensity(vertical: -1),
|
||||||
|
minLeadingWidth: 12,
|
||||||
|
leading: Icon(icon, size: 18),
|
||||||
|
title: Text(label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, // Make the text bold
|
||||||
|
)),
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
try {
|
||||||
|
await onTap();
|
||||||
|
} catch (e) {
|
||||||
|
showErrorMessage(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: enabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
double getProgress() {
|
double getProgress() {
|
||||||
final totalSize = task.meta.res?.size ?? 0;
|
final totalSize = task.meta.res?.size ?? 0;
|
||||||
return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize;
|
return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize;
|
||||||
@@ -172,71 +199,128 @@ class BuildTaskListView extends GetView {
|
|||||||
(total > 0 ? " / ${Util.fmtByte(total)}" : "");
|
(total > 0 ? " / ${Util.fmtByte(total)}" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
final taskController = Get.find<TaskController>();
|
||||||
elevation: 4.0,
|
final taskListController = taskController.tabIndex.value == 0
|
||||||
child: InkWell(
|
? Get.find<TaskDownloadingController>()
|
||||||
onTap: () {},
|
: Get.find<TaskDownloadedController>();
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
// Filter selected task ids that are still in the task list
|
||||||
children: [
|
filterSelectedTaskIds(Iterable<String> selectedTaskIds) => selectedTaskIds
|
||||||
ListTile(
|
.where((id) => tasks.any((task) => task.id == id))
|
||||||
title: Text(fileName(task.meta)),
|
.toList();
|
||||||
leading: (task.meta.res?.name.isNotEmpty ?? false
|
|
||||||
? const Icon(FaIcons.folder)
|
return ContextMenuArea(
|
||||||
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))]))),
|
width: 140,
|
||||||
Row(
|
builder: (context) => [
|
||||||
|
buildContextItem(Icons.checklist, 'selectAll'.tr, () {
|
||||||
|
if (tasks.isEmpty) return;
|
||||||
|
|
||||||
|
if (selectedTaskIds.isNotEmpty) {
|
||||||
|
taskListController.selectedTaskIds([]);
|
||||||
|
} else {
|
||||||
|
taskListController.selectedTaskIds(tasks.map((e) => e.id).toList());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
buildContextItem(Icons.check, 'select'.tr, () {
|
||||||
|
if (isSelect()) {
|
||||||
|
taskListController.selectedTaskIds(taskListController
|
||||||
|
.selectedTaskIds
|
||||||
|
.where((element) => element != task.id)
|
||||||
|
.toList());
|
||||||
|
} else {
|
||||||
|
taskListController.selectedTaskIds(
|
||||||
|
[...taskListController.selectedTaskIds, task.id]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
const Divider(
|
||||||
|
indent: 8,
|
||||||
|
endIndent: 8,
|
||||||
|
),
|
||||||
|
buildContextItem(Icons.play_arrow, 'continue'.tr, () async {
|
||||||
|
try {
|
||||||
|
await continueAllTasks(filterSelectedTaskIds(
|
||||||
|
{...taskListController.selectedTaskIds, task.id}));
|
||||||
|
} finally {
|
||||||
|
taskListController.selectedTaskIds([]);
|
||||||
|
}
|
||||||
|
}, enabled: !isDone() && !isRunning()),
|
||||||
|
buildContextItem(Icons.pause, 'pause'.tr, () async {
|
||||||
|
try {
|
||||||
|
await pauseAllTasks(filterSelectedTaskIds(
|
||||||
|
{...taskListController.selectedTaskIds, task.id}));
|
||||||
|
} finally {
|
||||||
|
taskListController.selectedTaskIds([]);
|
||||||
|
}
|
||||||
|
}, enabled: !isDone() && isRunning()),
|
||||||
|
buildContextItem(Icons.delete, 'delete'.tr, () async {
|
||||||
|
try {
|
||||||
|
await showDeleteDialog(filterSelectedTaskIds(
|
||||||
|
{...taskListController.selectedTaskIds, task.id}));
|
||||||
|
} finally {
|
||||||
|
taskListController.selectedTaskIds([]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
child: Obx(
|
||||||
|
() => Card(
|
||||||
|
elevation: 4.0,
|
||||||
|
shape: isSelect()
|
||||||
|
? RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
taskController.scaffoldKey.currentState?.openEndDrawer();
|
||||||
|
taskController.selectTask.value = task;
|
||||||
|
},
|
||||||
|
onDoubleTap: () {
|
||||||
|
task.open();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
ListTile(
|
||||||
flex: 1,
|
title: Text(task.name),
|
||||||
child: Text(
|
leading: Icon(
|
||||||
getProgressText(),
|
fileIcon(task.name,
|
||||||
style: Get.textTheme.bodyLarge
|
isFolder: isFolderTask(),
|
||||||
?.copyWith(color: Get.theme.disabledColor),
|
isBitTorrent: task.protocol == Protocol.bt),
|
||||||
).padding(left: 18)),
|
|
||||||
Expanded(
|
|
||||||
flex: 1,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text("${Util.fmtByte(task.progress.speed)} / s",
|
|
||||||
style: Get.textTheme.titleSmall),
|
|
||||||
...buildActions()
|
|
||||||
],
|
|
||||||
)),
|
)),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Text(
|
||||||
|
getProgressText(),
|
||||||
|
style: Get.textTheme.bodyLarge
|
||||||
|
?.copyWith(color: Get.theme.disabledColor),
|
||||||
|
).padding(left: 18)),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text("${Util.fmtByte(task.progress.speed)} / s",
|
||||||
|
style: Get.textTheme.titleSmall),
|
||||||
|
...buildActions()
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
isDone()
|
||||||
|
? Container()
|
||||||
|
: LinearProgressIndicator(
|
||||||
|
value: getProgress(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
isDone()
|
)).padding(horizontal: 14, top: 8),
|
||||||
? Container()
|
),
|
||||||
: LinearProgressIndicator(
|
);
|
||||||
value: getProgress(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)).padding(horizontal: 14, top: 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
String fileName(Meta meta) {
|
|
||||||
if (meta.opts.name.isNotEmpty) {
|
|
||||||
return meta.opts.name;
|
|
||||||
}
|
|
||||||
if (meta.res == null) {
|
|
||||||
final u = Uri.parse(meta.req.url);
|
|
||||||
if (u.scheme.startsWith("http")) {
|
|
||||||
return u.path.isNotEmpty
|
|
||||||
? u.path.substring(u.path.lastIndexOf("/") + 1)
|
|
||||||
: u.host;
|
|
||||||
} else {
|
|
||||||
final params = u.queryParameters;
|
|
||||||
if (params.containsKey("dn")) {
|
|
||||||
return params["dn"]!;
|
|
||||||
} else {
|
|
||||||
return params["xt"]!.split(":").last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (meta.res!.name.isNotEmpty) {
|
|
||||||
return meta.res!.name;
|
|
||||||
}
|
|
||||||
return meta.res!.files[0].name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CompactCheckbox extends StatefulWidget {
|
||||||
|
final String label;
|
||||||
|
final bool value;
|
||||||
|
final ValueChanged<bool?>? onChanged;
|
||||||
|
final double? scale;
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
const CompactCheckbox({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
this.onChanged,
|
||||||
|
this.scale,
|
||||||
|
this.textStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CompactCheckbox> createState() => _CompactCheckboxState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompactCheckboxState extends State<CompactCheckbox> {
|
||||||
|
late bool _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_value = widget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueChanged(bool? value) {
|
||||||
|
setState(() {
|
||||||
|
_value = value!;
|
||||||
|
});
|
||||||
|
widget.onChanged?.call(_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final checkbox = Checkbox(
|
||||||
|
value: _value,
|
||||||
|
onChanged: valueChanged,
|
||||||
|
);
|
||||||
|
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
valueChanged(!_value);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
widget.scale == null
|
||||||
|
? checkbox
|
||||||
|
: Transform.scale(
|
||||||
|
scale: widget.scale,
|
||||||
|
child: checkbox,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.label,
|
||||||
|
style: widget.textStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../../util/message.dart';
|
||||||
|
|
||||||
|
class CopyButton extends StatefulWidget {
|
||||||
|
final String? url;
|
||||||
|
|
||||||
|
const CopyButton(this.url, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CopyButton> createState() => _CopyButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CopyButtonState extends State<CopyButton> {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
copy() {
|
||||||
|
final url = widget.url;
|
||||||
|
if (url != null) {
|
||||||
|
try {
|
||||||
|
Clipboard.setData(ClipboardData(text: url));
|
||||||
|
setState(() {
|
||||||
|
success = true;
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
setState(() {
|
||||||
|
success = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
showErrorMessage(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
icon: success ? const Icon(Icons.check_circle) : const Icon(Icons.copy),
|
||||||
|
onPressed: copy,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,30 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
|
||||||
import '../../util/mac_secure_util.dart';
|
import '../../util/mac_secure_util.dart';
|
||||||
|
import '../../util/message.dart';
|
||||||
import '../../util/util.dart';
|
import '../../util/util.dart';
|
||||||
|
|
||||||
|
final deviceInfo = DeviceInfoPlugin();
|
||||||
|
|
||||||
class DirectorySelector extends StatefulWidget {
|
class DirectorySelector extends StatefulWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final bool showLabel;
|
final bool showLabel;
|
||||||
|
final bool showAndoirdToggle;
|
||||||
|
|
||||||
const DirectorySelector(
|
const DirectorySelector(
|
||||||
{Key? key, required this.controller, this.showLabel = true})
|
{Key? key,
|
||||||
|
required this.controller,
|
||||||
|
this.showLabel = true,
|
||||||
|
this.showAndoirdToggle = false})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -20,6 +34,82 @@ class DirectorySelector extends StatefulWidget {
|
|||||||
class _DirectorySelectorState extends State<DirectorySelector> {
|
class _DirectorySelectorState extends State<DirectorySelector> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Widget? buildSelectWidget() {
|
||||||
|
if (Util.isDesktop()) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
onPressed: () async {
|
||||||
|
var dir = await FilePicker.platform.getDirectoryPath();
|
||||||
|
if (dir != null) {
|
||||||
|
widget.controller.text = dir;
|
||||||
|
MacSecureUtil.saveBookmark(dir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// After Android 11, access to external storage is increasingly restricted, so it no longer supports selecting the download directory. However, if you do not download in external storage, all downloaded files will be deleted after the application is uninstalled.
|
||||||
|
// Fortunately, so far, most Android devices can still access the system download directory.
|
||||||
|
// For the sake of user experience, it is decided to only support selecting the application's internal directory and the system download directory. Also, a test for file write permission is performed when selecting the system download directory. If it cannot be written, selection is not allowed.
|
||||||
|
if (Util.isAndroid() && widget.showAndoirdToggle) {
|
||||||
|
final isSwitchToDownloadDir =
|
||||||
|
widget.controller.text.endsWith('/Gopeed');
|
||||||
|
|
||||||
|
return ToggleSwitch(
|
||||||
|
initialLabelIndex: isSwitchToDownloadDir ? 1 : 0,
|
||||||
|
totalSwitches: 2,
|
||||||
|
icons: const [Icons.home, Icons.download],
|
||||||
|
customWidths: const [50, 50],
|
||||||
|
onToggle: (index) async {
|
||||||
|
if (index == 0) {
|
||||||
|
widget.controller.text =
|
||||||
|
(await getExternalStorageDirectory())?.path ??
|
||||||
|
(await getApplicationDocumentsDirectory()).path;
|
||||||
|
} else {
|
||||||
|
widget.controller.text =
|
||||||
|
'${(await DownloadsPath.downloadsDirectory())!.path}/Gopeed';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancelToggle: (index) async {
|
||||||
|
if (index == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final downloadDir =
|
||||||
|
(await DownloadsPath.downloadsDirectory())?.path;
|
||||||
|
if (downloadDir == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and request external storage permission when sdk version < 30 (android 11)
|
||||||
|
if ((await deviceInfo.androidInfo).version.sdkInt < 30) {
|
||||||
|
var status = await Permission.storage.status;
|
||||||
|
if (!status.isGranted) {
|
||||||
|
status = await Permission.storage.request();
|
||||||
|
if (!status.isGranted) {
|
||||||
|
showErrorMessage('noStoragePermission'.tr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check write permission
|
||||||
|
final fileRandomeName =
|
||||||
|
"test_${DateTime.now().millisecondsSinceEpoch}.tmp";
|
||||||
|
final testFile = File('$downloadDir/Gopeed/$fileRandomeName');
|
||||||
|
try {
|
||||||
|
await testFile.create(recursive: true);
|
||||||
|
await testFile.writeAsString('test');
|
||||||
|
await testFile.delete();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
showErrorMessage(e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).marginOnly(left: 10);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -35,17 +125,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
|
|||||||
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
|
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
!Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
|
buildSelectWidget()
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.folder_open),
|
|
||||||
onPressed: () async {
|
|
||||||
var dir = await FilePicker.platform.getDirectoryPath();
|
|
||||||
if (dir != null) {
|
|
||||||
widget.controller.text = dir;
|
|
||||||
MacSecureUtil.saveBookmark(dir);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
].where((e) => e != null).map((e) => e!).toList(),
|
].where((e) => e != null).map((e) => e!).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../icon/gopeed_icons.dart';
|
||||||
|
|
||||||
|
final Map<IconData, List<String>> iconConfigMap = {
|
||||||
|
Gopeed.install: ['exe', 'msi', 'dmg', 'deb', 'rpm'],
|
||||||
|
Gopeed.android: ['apk'],
|
||||||
|
Gopeed.app_store_ios: ['ipa'],
|
||||||
|
Gopeed.file_bt: ['torrent'],
|
||||||
|
Gopeed.cd: ['iso'],
|
||||||
|
Gopeed.html5: ['html', 'htm'],
|
||||||
|
Gopeed.file_alt: ['txt', 'md', 'log', 'csv', 'tsv', 'json', 'yaml', 'yml'],
|
||||||
|
Gopeed.file_pdf: ['pdf'],
|
||||||
|
Gopeed.file_word: ['doc', 'docx'],
|
||||||
|
Gopeed.file_excel: ['xls', 'xlsx'],
|
||||||
|
Gopeed.file_powerpoint: ['ppt', 'pptx'],
|
||||||
|
Gopeed.file_archive: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'],
|
||||||
|
Gopeed.file_image: [
|
||||||
|
'jpg',
|
||||||
|
'jpeg',
|
||||||
|
'png',
|
||||||
|
'gif',
|
||||||
|
'bmp',
|
||||||
|
'tiff',
|
||||||
|
'svg',
|
||||||
|
'webp'
|
||||||
|
],
|
||||||
|
Gopeed.file_audio: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a'],
|
||||||
|
Gopeed.file_video: ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm'],
|
||||||
|
Gopeed.file_code: [
|
||||||
|
'js',
|
||||||
|
'css',
|
||||||
|
'json',
|
||||||
|
'xml',
|
||||||
|
'java',
|
||||||
|
'cpp',
|
||||||
|
'dart',
|
||||||
|
'py',
|
||||||
|
'rb',
|
||||||
|
'php',
|
||||||
|
'ts',
|
||||||
|
'swift',
|
||||||
|
'go',
|
||||||
|
'rs'
|
||||||
|
],
|
||||||
|
Gopeed.file: [''],
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<String, IconData> _iconCache = Map.fromEntries(
|
||||||
|
iconConfigMap.entries.expand(
|
||||||
|
(entry) => entry.value.map((ext) => MapEntry(ext, entry.key)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
String fileExt(String? name) {
|
||||||
|
if (name == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final ext = name.split('.').last;
|
||||||
|
if (ext.length > 8) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ext.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData fileIcon(String? name,
|
||||||
|
{bool isFolder = false, bool isBitTorrent = false}) {
|
||||||
|
if (isFolder) {
|
||||||
|
return isBitTorrent ? Gopeed.folder_bt : Gopeed.folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ext = fileExt(name);
|
||||||
|
return _iconCache[ext] ?? Gopeed.file;
|
||||||
|
}
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' as fluent;
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import '../../api/model/resource.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
|
|
||||||
import '../../util/file_icon.dart';
|
|
||||||
import '../../util/icons.dart';
|
|
||||||
import '../../util/util.dart';
|
|
||||||
import '../modules/app/controllers/app_controller.dart';
|
|
||||||
import '../modules/create/controllers/create_controller.dart';
|
|
||||||
|
|
||||||
class FileListView extends GetView {
|
|
||||||
final List<FileInfo> files;
|
|
||||||
|
|
||||||
FileListView({
|
|
||||||
Key? key,
|
|
||||||
required this.files,
|
|
||||||
}) : super(key: key);
|
|
||||||
final parentController = Get.find<CreateController>();
|
|
||||||
final appController = Get.find<AppController>();
|
|
||||||
|
|
||||||
List findChildFileIdsR(List children) {
|
|
||||||
List res = [];
|
|
||||||
for (var k in children) {
|
|
||||||
if (parentController.fileInfos[k]['type'] == 'folder') {
|
|
||||||
res.addAll(
|
|
||||||
findChildFileIdsR(parentController.fileInfos[k]['children']));
|
|
||||||
} else {
|
|
||||||
res.add(parentController.fileInfos[k]['fileId']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearDescendantFolderSizeR(int id) {
|
|
||||||
if (parentController.fileInfos[id]['type'] == 'folder') {
|
|
||||||
parentController.fileInfos[id]['size'] = 0;
|
|
||||||
for (var childId in parentController.fileInfos[id]['children']) {
|
|
||||||
clearDescendantFolderSizeR(childId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reduceAncestorFolderSizeR(int id, int size) {
|
|
||||||
int parentId = parentController.fileInfos[id]['parentId'];
|
|
||||||
|
|
||||||
if (parentId != -1) {
|
|
||||||
parentController.fileInfos[parentId]['size'] -= size;
|
|
||||||
reduceAncestorFolderSizeR(parentId, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int addDescendantFolderSizeR(int id) {
|
|
||||||
if (parentController.fileInfos[id]['type'] == 'folder') {
|
|
||||||
for (var childId in parentController.fileInfos[id]['children']) {
|
|
||||||
parentController.fileInfos[id]['size'] +=
|
|
||||||
addDescendantFolderSizeR(childId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parentController.fileInfos[id]['size'];
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAncestorFolderSizeR(int id, int size) {
|
|
||||||
int parentId = parentController.fileInfos[id]['parentId'];
|
|
||||||
if (parentId != -1) {
|
|
||||||
parentController.fileInfos[parentId]['size'] += size;
|
|
||||||
addAncestorFolderSizeR(parentId, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<fluent.TreeViewItem> buildTreeViewItemsR(int level, int parentId) {
|
|
||||||
List<fluent.TreeViewItem> res = [];
|
|
||||||
List children = parentController.fileInfos
|
|
||||||
.where((e) => e['level'] == level && e['parentId'] == parentId)
|
|
||||||
.toList();
|
|
||||||
for (int j = 0; j < children.length; j++) {
|
|
||||||
Map fileInfo = children[j];
|
|
||||||
|
|
||||||
if (fileInfo['type'] == 'folder') {
|
|
||||||
// folder
|
|
||||||
res.add(fluent.TreeViewItem(
|
|
||||||
// expanded: false, bug on init
|
|
||||||
value: fileInfo['id'],
|
|
||||||
onInvoked: (item, reason) async {
|
|
||||||
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
|
|
||||||
List childrenIds = findChildFileIdsR(fileInfo['children']);
|
|
||||||
if (item.selected == true) {
|
|
||||||
parentController.selectedIndexes.addAll(childrenIds.cast());
|
|
||||||
parentController.fileInfos[fileInfo['id']]['size'] = 0;
|
|
||||||
addDescendantFolderSizeR(item.value);
|
|
||||||
addAncestorFolderSizeR(item.value, fileInfo['size']);
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
} else {
|
|
||||||
parentController.selectedIndexes
|
|
||||||
.removeWhere((index) => childrenIds.contains(index));
|
|
||||||
reduceAncestorFolderSizeR(item.value, fileInfo['size']);
|
|
||||||
clearDescendantFolderSizeR(item.value);
|
|
||||||
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leading: Obx(() {
|
|
||||||
return parentController.openedFolders.contains(fileInfo['id'])
|
|
||||||
? const Icon(FaIcons.folder_open,
|
|
||||||
color: Colors.lightBlueAccent)
|
|
||||||
: const Icon(FaIcons.folder, color: Colors.lightBlueAccent);
|
|
||||||
}),
|
|
||||||
onExpandToggle: (fluent.TreeViewItem item, bool getExpanded) async {
|
|
||||||
getExpanded
|
|
||||||
? parentController.openedFolders.add(item.value)
|
|
||||||
: parentController.openedFolders.remove(item.value);
|
|
||||||
},
|
|
||||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
fileInfo['name'],
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
// style: context.textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
return parentController.fileInfos[fileInfo['id']]['size'] != 0
|
|
||||||
? Text(
|
|
||||||
Util.fmtByte(
|
|
||||||
parentController.fileInfos[fileInfo['id']]['size']),
|
|
||||||
// style: context.textTheme.labelMedium,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
children: buildTreeViewItemsR(
|
|
||||||
// parentController.fileInfos.where((e) => e['level'] > level).toList(),
|
|
||||||
level + 1,
|
|
||||||
fileInfo['id'])));
|
|
||||||
} else {
|
|
||||||
// file
|
|
||||||
res.add(fluent.TreeViewItem(
|
|
||||||
value: {'id': fileInfo['id'], 'fileId': fileInfo['fileId']},
|
|
||||||
selected: fileInfo['selected'],
|
|
||||||
onInvoked: (item, reason) async {
|
|
||||||
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
|
|
||||||
if (item.selected == true) {
|
|
||||||
parentController.selectedIndexes.add(item.value['fileId']);
|
|
||||||
addAncestorFolderSizeR(item.value['id'], fileInfo['size']);
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
} else {
|
|
||||||
parentController.selectedIndexes.remove(item.value['fileId']);
|
|
||||||
reduceAncestorFolderSizeR(item.value['id'], fileInfo['size']);
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
collapsable: false,
|
|
||||||
leading: fileInfo['name'].lastIndexOf('.') == -1
|
|
||||||
? Icon(
|
|
||||||
FaIcons.doc,
|
|
||||||
color: appController.downloaderConfig.value.extra.themeMode ==
|
|
||||||
'dark'
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black45,
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
FaIcons.allIcons[findIcon(fileInfo['name'])],
|
|
||||||
color: appController.downloaderConfig.value.extra.themeMode ==
|
|
||||||
'dark'
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black45,
|
|
||||||
),
|
|
||||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
fileInfo['name'],
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
// style: context.textTheme.titleSmall,
|
|
||||||
)),
|
|
||||||
fileInfo['size'] > 0
|
|
||||||
? Text(
|
|
||||||
Util.fmtByte(
|
|
||||||
fileInfo['size'],
|
|
||||||
),
|
|
||||||
// style: context.textTheme.labelMedium,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
]),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<fluent.TreeViewItem> get items {
|
|
||||||
List infos = [];
|
|
||||||
int idNext = 0;
|
|
||||||
List selectedFileIds = [];
|
|
||||||
List openedFolders = [];
|
|
||||||
//make fileInfos list
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
|
||||||
//parentId -1 means path root
|
|
||||||
int parentId = -1;
|
|
||||||
List folders = path.split(files[i].path);
|
|
||||||
for (var folder in folders) {
|
|
||||||
int indexInInfos = infos.lastIndexWhere((fileInfo) =>
|
|
||||||
fileInfo['name'] == folder && fileInfo['parentId'] == parentId);
|
|
||||||
|
|
||||||
if (indexInInfos != -1) {
|
|
||||||
// folder exists
|
|
||||||
parentId = infos[indexInInfos]['id'];
|
|
||||||
// add size
|
|
||||||
infos[indexInInfos]['size'] += files[i].size;
|
|
||||||
} else {
|
|
||||||
// parent not exist
|
|
||||||
// create one and add index to parent's children
|
|
||||||
infos.add({
|
|
||||||
'level': folders.indexOf(folder),
|
|
||||||
'id': idNext,
|
|
||||||
'type': 'folder',
|
|
||||||
'name': folder,
|
|
||||||
'size': files[i].size,
|
|
||||||
'parentId': parentId,
|
|
||||||
'children': [],
|
|
||||||
});
|
|
||||||
openedFolders.add(idNext);
|
|
||||||
if (parentId != -1) {
|
|
||||||
infos[parentId]['children'].add(idNext);
|
|
||||||
}
|
|
||||||
parentId = idNext;
|
|
||||||
idNext++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//add one file, add index to parent
|
|
||||||
infos.add({
|
|
||||||
'id': idNext,
|
|
||||||
'type': 'file',
|
|
||||||
'fileId': i,
|
|
||||||
'level': folders.length,
|
|
||||||
'name': files[i].name,
|
|
||||||
'size': files[i].size,
|
|
||||||
'parentId': parentId,
|
|
||||||
//TODO add unselected logic
|
|
||||||
'selected': true
|
|
||||||
});
|
|
||||||
selectedFileIds.add(i);
|
|
||||||
if (parentId != -1) {
|
|
||||||
infos[parentId]['children'].add(idNext);
|
|
||||||
}
|
|
||||||
idNext++;
|
|
||||||
}
|
|
||||||
parentController.fileInfos.value = infos;
|
|
||||||
parentController.selectedIndexes.value = selectedFileIds.cast();
|
|
||||||
parentController.openedFolders.value = openedFolders;
|
|
||||||
List<fluent.TreeViewItem> treeItems = buildTreeViewItemsR(0, -1);
|
|
||||||
return treeItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return fluent.FluentTheme(
|
|
||||||
data: appController.downloaderConfig.value.extra.themeMode == 'system'
|
|
||||||
? fluent.FluentThemeData(brightness: ui.window.platformBrightness)
|
|
||||||
: appController.downloaderConfig.value.extra.themeMode == 'light'
|
|
||||||
? fluent.FluentThemeData(brightness: Brightness.light)
|
|
||||||
: fluent.FluentThemeData(brightness: Brightness.dark),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
|
||||||
Text(
|
|
||||||
'selectFile'.tr,
|
|
||||||
// style: TextStyle(color: themeData.hintColor),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(top: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey, width: 1),
|
|
||||||
borderRadius: BorderRadius.circular(5)),
|
|
||||||
child: fluent.TreeView(
|
|
||||||
selectionMode: fluent.TreeViewSelectionMode.multiple,
|
|
||||||
shrinkWrap: false,
|
|
||||||
// addRepaintBoundaries: false,
|
|
||||||
usePrototypeItem: true,
|
|
||||||
// cacheExtent: 20,
|
|
||||||
// onItemInvoked: (item, reason) async => {},
|
|
||||||
onSecondaryTap: (item, details) async {
|
|
||||||
debugPrint(
|
|
||||||
'onSecondaryTap $item at ${details.globalPosition}');
|
|
||||||
},
|
|
||||||
// onSelectionChanged: (selectedItems) async => {},
|
|
||||||
narrowSpacing: true,
|
|
||||||
items: items,
|
|
||||||
// scrollPrimary: true,
|
|
||||||
))),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
import 'package:checkable_treeview/checkable_treeview.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
|
||||||
|
import '../../api/model/resource.dart';
|
||||||
|
import '../../icon/gopeed_icons.dart';
|
||||||
|
import '../../util/util.dart';
|
||||||
|
import 'file_icon.dart';
|
||||||
|
import 'sort_icon_button.dart';
|
||||||
|
|
||||||
|
const _toggleSwitchIcons = [
|
||||||
|
Gopeed.file_video,
|
||||||
|
Gopeed.file_audio,
|
||||||
|
Gopeed.file_image,
|
||||||
|
];
|
||||||
|
const _sizeGapWidth = 72.0;
|
||||||
|
|
||||||
|
class FileTreeView extends StatefulWidget {
|
||||||
|
final List<FileInfo> files;
|
||||||
|
final List<int> initialValues;
|
||||||
|
final Function(List<int>) onSelectionChanged;
|
||||||
|
|
||||||
|
const FileTreeView(
|
||||||
|
{Key? key,
|
||||||
|
required this.files,
|
||||||
|
required this.initialValues,
|
||||||
|
required this.onSelectionChanged})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FileTreeView> createState() => _FileTreeViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileTreeViewState extends State<FileTreeView> {
|
||||||
|
late GlobalKey<TreeViewState<int>> key;
|
||||||
|
late int totalSize;
|
||||||
|
int? toggleSwitchIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
key = GlobalKey<TreeViewState<int>>();
|
||||||
|
totalSize = widget.files
|
||||||
|
.fold(0, (previousValue, element) => previousValue + element.size);
|
||||||
|
widget.onSelectionChanged(widget.initialValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectedFileCount =
|
||||||
|
key.currentState?.getSelectedValues().where((e) => e != null).length ??
|
||||||
|
widget.files.length;
|
||||||
|
final selectedFileSize = calcSelectedSize(null);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Theme.of(context).dividerColor),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: TreeView(
|
||||||
|
key: key,
|
||||||
|
nodes: buildTreeNodes(),
|
||||||
|
showExpandCollapseButton: true,
|
||||||
|
showSelectAll: true,
|
||||||
|
onSelectionChanged: (selectedValues) {
|
||||||
|
setState(() {});
|
||||||
|
widget.onSelectionChanged(selectedValues
|
||||||
|
.where((e) => e != null)
|
||||||
|
.map((e) => e!)
|
||||||
|
.toList());
|
||||||
|
},
|
||||||
|
selectAllTrailing: (context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('name'.tr),
|
||||||
|
SortIconButton(
|
||||||
|
onStateChanged: (state) {
|
||||||
|
switch (state) {
|
||||||
|
case SortState.asc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return (p0.label as Text)
|
||||||
|
.data!
|
||||||
|
.compareTo((p1.label as Text).data!);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case SortState.desc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return (p1.label as Text)
|
||||||
|
.data!
|
||||||
|
.compareTo((p0.label as Text).data!);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
key.currentState?.sort(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('size'.tr),
|
||||||
|
SortIconButton(
|
||||||
|
onStateChanged: (state) {
|
||||||
|
switch (state) {
|
||||||
|
case SortState.asc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return calcSelectedSize(p0)
|
||||||
|
.compareTo(calcSelectedSize(p1));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case SortState.desc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return calcSelectedSize(p1)
|
||||||
|
.compareTo(calcSelectedSize(p0));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
key.currentState?.sort(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 4,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {},
|
||||||
|
child: ToggleSwitch(
|
||||||
|
minHeight: 32,
|
||||||
|
cornerRadius: 8,
|
||||||
|
doubleTapDisable: true,
|
||||||
|
inactiveBgColor: Theme.of(context).dividerColor,
|
||||||
|
activeBgColor: [Theme.of(context).colorScheme.primary],
|
||||||
|
initialLabelIndex: toggleSwitchIndex,
|
||||||
|
icons: _toggleSwitchIcons,
|
||||||
|
onToggle: (index) {
|
||||||
|
toggleSwitchIndex = index;
|
||||||
|
if (index == null) {
|
||||||
|
key.currentState?.setSelectedValues(List.empty());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final iconFileExtArr =
|
||||||
|
iconConfigMap[_toggleSwitchIcons[index]] ?? [];
|
||||||
|
final selectedFileIndexes = widget.files
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.where((e) =>
|
||||||
|
iconFileExtArr.contains(fileExt(e.value.name)))
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
key.currentState?.setSelectedValues(selectedFileIndexes);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 6,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('fileSelectedCount'.tr),
|
||||||
|
Text(
|
||||||
|
selectedFileCount.toString(),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('fileSelectedSize'.tr),
|
||||||
|
Text(
|
||||||
|
selectedFileCount > 0 && selectedFileSize == 0
|
||||||
|
? 'unknown'.tr
|
||||||
|
: Util.fmtByte(selectedFileSize),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int calcSelectedSize(TreeNode<int>? node) {
|
||||||
|
if (key.currentState == null) {
|
||||||
|
return widget.files
|
||||||
|
.fold(0, (previousValue, element) => previousValue + element.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedFileIndexes = node == null
|
||||||
|
? key.currentState?.getSelectedValues()
|
||||||
|
: (node.value != null
|
||||||
|
? [node.value]
|
||||||
|
: key.currentState?.getChildSelectedValues(node));
|
||||||
|
|
||||||
|
if (selectedFileIndexes == null) return 0;
|
||||||
|
return selectedFileIndexes.where((e) => e != null).map((e) => e!).fold(0,
|
||||||
|
(previousValue, element) => previousValue + widget.files[element].size);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TreeNode<int>> buildTreeNodes() {
|
||||||
|
final List<TreeNode<int>> rootNodes = [];
|
||||||
|
final Map<String, TreeNode<int>> dirNodes = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < widget.files.length; i++) {
|
||||||
|
final file = widget.files[i];
|
||||||
|
final parts = file.path.split('/');
|
||||||
|
String currentPath = '';
|
||||||
|
TreeNode<int>? parentNode;
|
||||||
|
|
||||||
|
// Create or get directory nodes
|
||||||
|
for (final part in parts) {
|
||||||
|
if (part.isEmpty) continue;
|
||||||
|
|
||||||
|
currentPath += '/$part';
|
||||||
|
if (!dirNodes.containsKey(currentPath)) {
|
||||||
|
final node = TreeNode<int>(
|
||||||
|
label: Text(part),
|
||||||
|
icon: Icon(
|
||||||
|
fileIcon(part, isFolder: true),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
trailing: (context, node) {
|
||||||
|
final size = calcSelectedSize(node);
|
||||||
|
return size > 0
|
||||||
|
? Text(Util.fmtByte(calcSelectedSize(node)),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall)
|
||||||
|
: const SizedBox(width: _sizeGapWidth);
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
dirNodes[currentPath] = node;
|
||||||
|
|
||||||
|
if (parentNode == null) {
|
||||||
|
rootNodes.add(node);
|
||||||
|
} else {
|
||||||
|
parentNode.children.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentNode = dirNodes[currentPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create file node using file.name
|
||||||
|
final fileNode = TreeNode<int>(
|
||||||
|
label: Text(file.name),
|
||||||
|
value: i,
|
||||||
|
icon: Icon(fileIcon(file.name, isFolder: false), size: 18),
|
||||||
|
trailing: (context, node) {
|
||||||
|
return file.size > 0
|
||||||
|
? Text(Util.fmtByte(file.size),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall)
|
||||||
|
: const SizedBox(width: _sizeGapWidth);
|
||||||
|
},
|
||||||
|
isSelected: widget.initialValues.contains(i),
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add file node to parent or root
|
||||||
|
if (parentNode != null) {
|
||||||
|
parentNode.children.add(fileNode);
|
||||||
|
} else {
|
||||||
|
rootNodes.add(fileNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../icon/gopeed_icons.dart';
|
||||||
|
|
||||||
|
enum SortState { none, asc, desc }
|
||||||
|
|
||||||
|
class SortIconButton extends StatefulWidget {
|
||||||
|
final double size;
|
||||||
|
final Color? color;
|
||||||
|
final Color? activeColor;
|
||||||
|
final SortState initialState;
|
||||||
|
final Function(SortState) onStateChanged;
|
||||||
|
|
||||||
|
const SortIconButton({
|
||||||
|
Key? key,
|
||||||
|
this.size = 18.0,
|
||||||
|
this.color,
|
||||||
|
this.activeColor,
|
||||||
|
this.initialState = SortState.none,
|
||||||
|
required this.onStateChanged,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SortIconButton> createState() => _SortIconState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SortIconState extends State<SortIconButton> {
|
||||||
|
late SortState _currentState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentState = widget.initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleState() {
|
||||||
|
setState(() {
|
||||||
|
switch (_currentState) {
|
||||||
|
case SortState.none:
|
||||||
|
_currentState = SortState.asc;
|
||||||
|
break;
|
||||||
|
case SortState.asc:
|
||||||
|
_currentState = SortState.desc;
|
||||||
|
break;
|
||||||
|
case SortState.desc:
|
||||||
|
_currentState = SortState.none;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
widget.onStateChanged(_currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
onTap: _toggleState,
|
||||||
|
child: SizedBox(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
child: _currentState == SortState.none
|
||||||
|
? Icon(
|
||||||
|
Gopeed.sort,
|
||||||
|
size: widget.size,
|
||||||
|
color: widget.color,
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
ClipRect(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
heightFactor: 0.5,
|
||||||
|
child: Icon(
|
||||||
|
Gopeed.sort,
|
||||||
|
size: widget.size,
|
||||||
|
color: _currentState == SortState.asc
|
||||||
|
? (widget.activeColor ??
|
||||||
|
Theme.of(context).colorScheme.primary)
|
||||||
|
: widget.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ClipRect(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
heightFactor: 0.5,
|
||||||
|
child: Icon(
|
||||||
|
Gopeed.sort,
|
||||||
|
size: widget.size,
|
||||||
|
color: _currentState == SortState.desc
|
||||||
|
? (widget.activeColor ??
|
||||||
|
Theme.of(context).colorScheme.primary)
|
||||||
|
: widget.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user