Compare commits

...
69 Commits
Author SHA1 Message Date
Levi 81dfc073a6 release: v1.6.4 2024-12-09 20:30:33 +08:00
Levi 427921ccab perf: write to disk only when progress updated (#839) 2024-12-09 20:29:50 +08:00
Levi f4876c8a0e fix: HTTP fetch crash when modified request extra via extension (#832) 2024-12-08 12:07:08 +08:00
Navy.Wang 35a3625b11 feat: add QNAP platform build (#828) 2024-11-29 13:38:17 +08:00
ilkei dc9b501e35 feat: optimize docker build (#827) 2024-11-27 13:40:28 +08:00
Levi 85fe92a378 release: v1.6.3 2024-11-18 18:54:46 +08:00
Levi 983d9ad09e fix: windows location fix patch not effect (#819) 2024-11-18 18:54:15 +08:00
Levi d9f757aaf0 fix: revert flutter version to v3.16.8 (#821) 2024-11-18 18:31:25 +08:00
Levi cd409fcfb9 ci: add build tests for all platforms (#812) 2024-11-15 12:06:51 +08:00
Levi 366244fa5d fix: android build on go 1.23 (#813) 2024-11-15 11:43:55 +08:00
Levi 7a2e9b4fd1 release: v1.6.2 2024-11-14 23:45:08 +08:00
Levi 203385d3f8 fix: use golang 1.23 standard library to collect crash logs (#807) 2024-11-14 00:54:34 +08:00
Levi aaf73dec24 fix: proxy config reload panic (#808) 2024-11-14 00:36:21 +08:00
Ricky 2523282a89 feat(http): add option to skip SSL certificate verification (#801) 2024-11-12 10:30:21 +08:00
Levi 3384f5197a fix: download path in docker-compose.yml (#798) 2024-11-05 09:55:25 +08:00
Levi cd77665f02 fix: avoid request locatoin permisson on windows (#787) 2024-10-19 16:55:23 +08:00
Levi a22b431150 feat: upgrade flutter to v3.24.3 (#791) 2024-10-19 12:12:41 +08:00
Levi eb7c9ad7f2 release: v1.6.1 2024-10-16 18:31:56 +08:00
Levi ea11fc38f1 ci: fix windows build version (#785) 2024-10-16 18:30:41 +08:00
towermom9 1be16e9f2c feat: Updated the indonesian translation (#777) 2024-10-10 13:18:11 +08:00
Ayase 1d86d14a20 fix: open files on linux (#765) 2024-10-03 21:40:49 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣 5ad15cc4ac feat: Update tr_tr.dart (#754) 2024-09-23 09:19:45 +08:00
Levi 49604966fd release: v1.6.0 2024-09-21 11:00:47 +08:00
Levi bdb8ed177f feat: supports batch operation of tasks by filter (#753) 2024-09-21 10:19:52 +08:00
Levi 2dc7048cba feat: support download directory white list (#752) 2024-09-20 21:32:21 +08:00
Levi d3750464aa fix: torrent stats panic (#737) 2024-09-20 12:28:24 +08:00
Levi 5c9bcd3731 fix: DotAll flag not working with group (#750) 2024-09-19 21:28:46 +08:00
Aman Raj Singh Mourya bdda550b2c feat: avoid using the depreciated background (#746) 2024-09-18 14:39:01 +08:00
liwei 3308d966f0 docs: fix banner transparent 2024-09-13 18:40:58 +08:00
Levi 18041ed9ec docs: improve banner image (#740) 2024-09-11 19:02:39 +08:00
Levi 56b85e545e release: v1.5.9 2024-09-07 19:20:12 +08:00
Levi c5af916952 feat: support default direct download config (#735) 2024-09-07 14:08:17 +08:00
Levi a1981328aa fix: avoid HTTP zero connections (#729) 2024-09-07 13:49:32 +08:00
Levi 51244133e0 feat: support extension onDone event (#728) 2024-09-07 10:10:39 +08:00
Levi 0c17093b1a feat: task separate proxy (#727) 2024-09-06 18:35:23 +08:00
Levi 6538b29b62 feat: macos round icon (#719) 2024-09-03 18:47:49 +08:00
Levi 81e90e6b89 fix: try request grant external storage on Android (#660) 2024-09-03 18:47:06 +08:00
JuanQ 4f39296664 feat: Add spanish language (#667) 2024-08-27 08:59:18 +08:00
Levi ece5b91dc2 fix: torrent redundant directory (#649) 2024-08-14 21:54:59 +08:00
Levi d9f04ae928 fix: tasks block when paused while not ready (#648) 2024-08-14 21:22:23 +08:00
Levi e3ccecbd9c fix: task directory open (#647) 2024-08-14 21:21:41 +08:00
Levi fb1c2cb060 fix: downgrade minSdkVersion (#642) 2024-08-14 21:21:09 +08:00
Levi 9bff75464d ci: docker flutter build on host (#637) 2024-08-09 18:28:44 +08:00
Levi 863f759248 release: v1.5.8 2024-08-09 12:57:11 +08:00
Levi f89fb5e438 fix: web config file not loaded when is relative path (#636) 2024-08-09 12:50:20 +08:00
Levi 7ca1cc8c64 fix: batch create tasks with duplicate names (#635) 2024-08-09 12:41:03 +08:00
Levi 1f791c66e6 perf: excessive torrent tracker dns resolve (#634) 2024-08-08 18:55:52 +08:00
Levi 4d60c47d39 feat: task detail view (#633) 2024-08-08 18:34:00 +08:00
Levi 3fd3dd49d2 feat(extension): get exception stacks on promise error (#630) 2024-08-07 12:04:45 +08:00
foxxorcat d119de7b98 feat: enhanced extension engine (#629) 2024-08-05 10:44:17 +08:00
madoka773 69b359a9c9 fix: tray icon not shown on linux flatpak or snap (#625) 2024-08-02 12:36:25 +08:00
Levi 4fbe13a1f0 fix: clear UPNP port mapping when exist app (#624) 2024-07-30 18:43:54 +08:00
Levi 3b50465708 feat: torrent seed config (#599) 2024-07-30 12:50:06 +08:00
Levi 473c58796c fix: handle empty string params (#620) 2024-07-24 11:51:31 +08:00
abc0922001 fab8f135f3 feat: Update Traditional Chinese translation (#611) 2024-07-17 18:42:42 +08:00
liwei 0fca7b6bc7 fix: fr_fr import 2024-07-15 09:36:36 +08:00
torik988 4700d0ba28 feat: Create fr_fr.dart (#608) 2024-07-15 09:28:03 +08:00
towermom9 6e31ea089f feat: Indonesian Translation/Localization (#606) 2024-07-15 09:21:49 +08:00
Levi 4a2088f6fb fix: xhr string request body can specify Content-Type (#596) 2024-07-09 18:40:02 +08:00
𝗪𝗜𝗡𝗭𝗢𝗥𝗧 17c8fb7e4d feat: update tr_tr.dart (#593) 2024-07-03 09:52:01 +08:00
Levi 8c99bbc847 ci: create CODEOWNERS 2024-06-26 17:17:27 +08:00
Levi 06c7fe7a22 fix: avoid hanging the main thread on repeat start (#584) 2024-06-20 18:59:50 +08:00
Levi 8b3b5d4394 fix: javascript engine xhr polyfill (#581) 2024-06-15 11:40:12 +08:00
liwei 2ee73ab619 fix: it_it translate 2024-06-11 14:37:39 +08:00
albanobattistella 15413dbc95 feat: Create it_it.dart (#576) 2024-06-11 14:23:24 +08:00
Levi c7c43fac80 fix: snap dir pick depend (#558) 2024-06-11 12:35:57 +08:00
247b 4283bdfa5a feat :tamil translation is added (#563) 2024-05-27 09:24:45 +08:00
liwei 328ba8592c build: update go-ieproxy depend 2024-05-23 08:57:02 +08:00
er2de2 a191a4140d feat: Update pl_pl.dart (#554) 2024-05-23 08:53:48 +08:00
114 changed files with 5796 additions and 1317 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
.git
*.data
*.log
node_modules
**/node_modules
Dockerfile
.dockerignore
.github
_docs
_examples
bin
ui
+1
View File
@@ -0,0 +1 @@
* @monkeyWie
+3
View File
@@ -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'
+54 -5
View File
@@ -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
@@ -299,6 +299,7 @@ jobs:
extensions: [gnome] extensions: [gnome]
plugs: plugs:
- network - network
- home
slots: slots:
- dbus-gopeed - dbus-gopeed
parts: parts:
@@ -324,8 +325,12 @@ jobs:
- xz-utils - xz-utils
- zip - zip
override-prime: '' override-prime: ''
zenity:
plugin: nil
stage-packages:
- zenity
gopeed: gopeed:
after: [flutter-git] after: [flutter-git,zenity]
source: . source: .
plugin: nil plugin: nil
stage-packages: stage-packages:
@@ -375,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
@@ -436,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 ../../
@@ -463,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:
@@ -478,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
@@ -488,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 }}
@@ -502,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 }}
+1 -1
View File
@@ -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
View File
@@ -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 .
+1
View File
@@ -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
View File
@@ -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"]
+5 -3
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <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>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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" -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
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <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>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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" -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
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Trạng thái kiểm tra](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Trạng thái kiểm tra](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Phiên bản](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Phiên bản](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <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>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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" -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
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <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>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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" -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
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <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>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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" -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
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
chown -R ${PUID}:${PGID} /app chown -R ${PUID}:${PGID} /app
+72 -65
View File
@@ -1,120 +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.11 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 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
go.opentelemetry.io/otel v1.22.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect
golang.org/x/crypto v0.18.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.17.0 // indirect golang.org/x/tools v0.27.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
) )
replace github.com/mattn/go-ieproxy => github.com/GopeedLab/go-ieproxy v0.0.0-20240508033154-86b7091da074
+226 -196
View File
@@ -7,18 +7,20 @@ 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= 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/GopeedLab/go-ieproxy v0.0.0-20240508033154-86b7091da074 h1:WTv/RQaUwPUWxX26kcRFp/kvSapYSKnw/viPtTH1ZTY= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/GopeedLab/go-ieproxy v0.0.0-20240508033154-86b7091da074/go.mod h1:/NsJd+kxZBmjMc5hrJCKMbP57B84rvq9BiDRbtO9AS0= 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=
@@ -35,26 +37,27 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
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=
@@ -67,27 +70,28 @@ 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=
@@ -104,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=
@@ -123,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=
@@ -142,8 +143,8 @@ 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/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=
@@ -155,7 +156,6 @@ github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
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/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/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=
@@ -174,21 +174,24 @@ github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+
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=
@@ -207,29 +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/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=
@@ -240,8 +244,8 @@ 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/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@@ -250,20 +254,29 @@ github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbc
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=
@@ -282,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=
@@ -371,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=
@@ -378,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=
@@ -397,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=
@@ -409,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=
@@ -442,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=
@@ -461,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=
@@ -481,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=
@@ -494,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=
@@ -523,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=
@@ -537,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=
@@ -557,51 +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/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
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=
@@ -609,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=
@@ -635,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=
@@ -659,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=
+6 -5
View File
@@ -2,13 +2,15 @@ package controller
import ( import (
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
) )
type Controller struct { type Controller struct {
GetConfig func(v any) bool GetConfig func(v any)
ProxyConfig *base.DownloaderProxyConfig 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
},
} }
} }
+52 -8
View File
@@ -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 {
+97
View File
@@ -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)
}
})
}
}
+6
View File
@@ -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)
}
}
}
+296 -87
View File
@@ -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,6 +11,7 @@ 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/url"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
@@ -22,6 +24,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 {
@@ -30,15 +34,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) {
@@ -46,13 +48,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
} }
@@ -63,12 +64,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
cfg.HTTPProxy = f.ctl.ProxyConfig.ToHandler() cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
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) {
@@ -78,46 +83,60 @@ 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
} }
func (f *Fetcher) Resolve(req *base.Request) error { func (f *Fetcher) Resolve(req *base.Request) error {
if err := f.addTorrent(req); err != nil { if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
f.meta.Req = req
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 {
@@ -126,7 +145,6 @@ func (f *Fetcher) Start() (err error) {
file.Download() file.Download()
} }
} }
return return
} }
@@ -137,8 +155,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
} }
@@ -151,80 +173,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(),
} }
} }
@@ -235,17 +271,95 @@ func (f *Fetcher) updateRes() {
} }
} }
func (f *Fetcher) addTorrent(req *base.Request) (err error) { func (f *Fetcher) Upload() (err error) {
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil { return f.addTorrent(f.meta.Req, true)
}
func (f *Fetcher) doUpload(fromUpload bool) {
if !f.torrentUpload.CompareAndSwap(false, true) {
return 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 if schema == "DATA" {
_, data := util.ParseDataUri(req.URL) _, data := util.ParseDataUri(req.URL)
buf := bytes.NewBuffer(data) buf := bytes.NewBuffer(data)
var metaInfo *metainfo.MetaInfo var metaInfo *metainfo.MetaInfo
@@ -285,34 +399,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, ".", "")
+128 -33
View File
@@ -8,6 +8,8 @@ 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"
gohttp "net/http"
"net/url"
"os" "os"
"reflect" "reflect"
"testing" "testing"
@@ -33,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{
@@ -69,57 +70,151 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
} }
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() 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() got = %v, want %v", fetcher.Meta().Res, want)
}
})
}
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(proxyConfig *base.DownloaderProxyConfig) 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.ProxyConfig = proxyConfig
fetcher.Setup(newController) fetcher.Setup(newController)
return fetcher return fetcher
} }
+2 -1
View File
@@ -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 {
Binary file not shown.
+82 -38
View File
@@ -3,14 +3,9 @@ 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/http" "net/http"
@@ -21,6 +16,13 @@ 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"
) )
type RequestError struct { type RequestError struct {
@@ -64,23 +66,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 +80,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,7 +101,7 @@ 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 // 解析资源大小: bytes 0-1000/1001 => 1001
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange)) contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
@@ -120,7 +113,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)
@@ -161,7 +155,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 +162,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 +169,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 +195,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()
} }
@@ -386,7 +386,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 +399,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 +407,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 {
@@ -452,7 +452,10 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
func (f *Fetcher) buildClient() *http.Client { func (f *Fetcher) buildClient() *http.Client {
transport := &http.Transport{ transport := &http.Transport{
Proxy: f.ctl.ProxyConfig.ToHandler(), 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)
@@ -466,30 +469,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)
@@ -500,3 +540,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
return fetcher return fetcher
} }
} }
func (fm *FetcherManager) Close() error {
return nil
}
+67 -13
View File
@@ -9,6 +9,8 @@ import (
"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"
"net" "net"
gohttp "net/http"
"net/url"
"testing" "testing"
"time" "time"
) )
@@ -192,6 +194,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)
} }
@@ -306,7 +356,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 {
@@ -337,10 +387,12 @@ 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.ProxyConfig = &base.DownloaderProxyConfig{ ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
Enable: true, return (&base.DownloaderProxyConfig{
Scheme: "socks5", Enable: true,
Host: proxyListener.Addr().String(), Scheme: "socks5",
Host: proxyListener.Addr().String(),
}).ToHandler()
} }
fetcher.Setup(ctl) fetcher.Setup(ctl)
err := fetcher.Start() err := fetcher.Start()
@@ -359,19 +411,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
+1 -1
View File
@@ -2,12 +2,12 @@ package test
import ( import (
"context" "context"
"crypto/rand"
"encoding/json" "encoding/json"
"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"
-8
View File
@@ -1,8 +0,0 @@
package base
import "errors"
var (
NotFound = errors.New("not found")
BadParams = errors.New("bad params")
)
+73 -8
View File
@@ -7,6 +7,7 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"net/http" "net/http"
"net/url" "net/url"
"sync"
"time" "time"
) )
@@ -16,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 {
@@ -25,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
@@ -94,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 {
@@ -147,12 +178,37 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.MaxRunning == 0 { if cfg.MaxRunning == 0 {
cfg.MaxRunning = 5 cfg.MaxRunning = 5
} }
if cfg.ProtocolConfig == nil {
cfg.ProtocolConfig = make(map[string]any)
}
if cfg.Proxy == nil { if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{} cfg.Proxy = &DownloaderProxyConfig{}
} }
return cfg 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 { type DownloaderProxyConfig struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
// System is the flag that use system proxy // System is the flag that use system proxy
@@ -168,7 +224,7 @@ func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, e
return nil return nil
} }
if cfg.System { if cfg.System {
ieproxy.ReloadConf() safeProxyReloadConf()
return ieproxy.GetProxyFunc() return ieproxy.GetProxyFunc()
} }
if cfg.Scheme == "" || cfg.Host == "" { if cfg.Scheme == "" || cfg.Host == "" {
@@ -183,7 +239,7 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
return nil return nil
} }
if cfg.System { if cfg.System {
ieproxy.ReloadConf() safeProxyReloadConf()
static := ieproxy.GetConf().Static static := ieproxy.GetConf().Static
if static.Active && len(static.Protocols) > 0 { if static.Active && len(static.Protocols) > 0 {
// If only one protocol, use it // If only one protocol, use it
@@ -209,6 +265,15 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd) 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 { func parseUrlSafe(rawUrl string) *url.URL {
u, err := url.Parse(rawUrl) u, err := url.Parse(rawUrl)
if err != nil { if err != nil {
+264
View File
@@ -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)
}
})
}
}
+411 -203
View File
@@ -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"))
@@ -124,11 +125,14 @@ func (d *Downloader) Setup() error {
} }
// 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.ProxyConfig = d.cfg.Proxy
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 {
@@ -304,7 +361,7 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) { func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
taskIds := make([]string, 0) taskIds := make([]string, 0)
for _, req := range reqs { for _, req := range reqs {
taskId, err := d.CreateDirect(req, opts) taskId, err := d.CreateDirect(req, opts.Clone())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -328,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()
@@ -395,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() {
@@ -416,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
} }
} }
@@ -425,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()
@@ -487,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
@@ -498,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
} }
@@ -539,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 {
@@ -599,17 +711,51 @@ 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
@@ -641,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 {
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()
@@ -662,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()
@@ -695,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{}
} }
@@ -742,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()
@@ -752,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 {
@@ -807,7 +1012,7 @@ 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
@@ -840,26 +1045,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)
@@ -867,10 +1074,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)
@@ -936,16 +1143,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
} }
@@ -955,6 +1162,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)
+273 -20
View File
@@ -5,6 +5,8 @@ import (
"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" "os"
"strconv"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -80,7 +82,45 @@ 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")
} }
} }
@@ -92,16 +132,34 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
if err := downloader.Setup(); err != nil { if err := downloader.Setup(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer downloader.Clear() defer func() {
downloader.Delete(nil, true)
downloader.Clear()
}()
reqs := make([]*base.Request, 0) reqs := make([]*base.Request, 0)
for i := 0; i < 6; i++ { fileNames := make([]string, 0)
for i := 0; i < 5; i++ {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
reqs = append(reqs, req) reqs = append(reqs, 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{ _, err := downloader.CreateDirectBatch(reqs, &base.Options{
Path: test.Dir, Path: test.Dir,
Name: test.DownloadName, Name: test.DownloadName,
@@ -113,29 +171,47 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
wg.Wait()
tasks := downloader.GetTasks() tasks := downloader.GetTasks()
if len(tasks) != len(reqs) { if len(tasks) != len(reqs) {
t.Errorf("CreateDirectBatch() got = %v, want %v", 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 *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return nil return nil
}, nil) }, nil)
// Disable proxy // Disable proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Enable = false proxyCfg.Enable = false
return proxyCfg return proxyCfg
}, nil) }, nil)
// Enable system proxy but not set proxy environment variable // Enable system proxy but not set proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.System = true proxyCfg.System = true
return proxyCfg return proxyCfg
}, nil) }, nil)
// Enable proxy but error proxy environment variable // Enable proxy but error proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234") os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234") os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
proxyCfg.System = true proxyCfg.System = true
@@ -146,33 +222,50 @@ func TestDownloader_CreateWithProxy(t *testing.T) {
} }
}) })
// Enable system proxy and set proxy environment variable // Enable system proxy and set proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String()) os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String()) os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
proxyCfg.System = true proxyCfg.System = true
return proxyCfg return proxyCfg
}, nil) }, nil)
// Invalid proxy scheme // Invalid proxy scheme
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Scheme = "" proxyCfg.Scheme = ""
return proxyCfg return proxyCfg
}, nil) }, nil)
// Invalid proxy host // Invalid proxy host
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Host = "" proxyCfg.Host = ""
return proxyCfg return proxyCfg
}, nil) }, nil)
// Use proxy without auth // Use proxy without auth
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return proxyCfg return proxyCfg
}, nil) }, nil)
// Use proxy with auth // Use proxy with auth
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return proxyCfg return proxyCfg
}, nil) }, 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 *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) { 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)) {
usr, pwd := "", "" usr, pwd := "", ""
if auth { if auth {
usr, pwd = "admin", "123" usr, pwd = "admin", "123"
@@ -185,17 +278,29 @@ 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(&base.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: test.ExternalDownloadUrl, 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 {
if errHandler == nil { if errHandler == nil {
@@ -295,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)
} }
@@ -319,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)
@@ -342,8 +447,8 @@ 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 := &base.DownloaderStoreConfig{ storeCfg := &base.DownloaderStoreConfig{
@@ -380,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)
}
})
}
+14 -2
View File
@@ -106,7 +106,7 @@ 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 {
@@ -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)
} }
} }
} }
+111 -8
View File
@@ -6,12 +6,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"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"
"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")
@@ -328,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)
@@ -367,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
+72 -42
View File
@@ -3,16 +3,18 @@ package xhr
import ( import (
"bytes" "bytes"
"errors" "errors"
"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"
"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 (
@@ -24,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"`
@@ -118,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
proxyHandler func(r *http.Request) (*url.URL, error) 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"`
} }
@@ -137,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) {
@@ -153,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() {
@@ -194,21 +212,32 @@ 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 {
req.Header.Set(k, v)
}
transport := &http.Transport{ transport := &http.Transport{
Proxy: xhr.proxyHandler, Proxy: xhr.proxyHandler,
} }
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 {
@@ -230,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)
@@ -261,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 {
@@ -269,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()
@@ -387,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
@@ -398,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
View File
@@ -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
File diff suppressed because it is too large. Load diff
+5 -2
View File
@@ -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"
+45 -10
View File
@@ -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 {
@@ -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{
@@ -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,43 @@ type OnErrorContext struct {
Error error `json:"error"` Error error `json:"error"`
} }
type ExtensionTask struct { type OnDoneContext struct {
*Task Task *Task `json:"task"`
}
type ExtensionTask struct {
download *Downloader download *Downloader
task *Task
Meta *ExtensionTaskMeta `json:"meta"`
}
// ExtensionTaskMeta restricts extension scripts to only modify request info
type ExtensionTaskMeta struct {
Req *base.Request `json:"req"`
} }
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask { func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
newTask := task.clone()
return &ExtensionTask{ return &ExtensionTask{
Task: task.clone(), task: newTask,
download: download, download: download,
Meta: &ExtensionTaskMeta{
Req: task.Meta.Req,
},
} }
} }
func (t *ExtensionTask) Continue() error { func (t *ExtensionTask) Continue() error {
return t.download.Continue(t.ID) return t.download.Continue(&TaskFilter{
IDs: []string{t.task.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.task.ID},
})
} }
func parseSettings(settings []*Setting) map[string]any { func parseSettings(settings []*Setting) map[string]any {
+39 -2
View File
@@ -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)
+72 -24
View File
@@ -1,6 +1,7 @@
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"
@@ -19,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 {
@@ -47,41 +50,86 @@ 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
@@ -92,10 +140,10 @@ 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 {
+5
View File
@@ -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';
});
+16
View File
@@ -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"
}
]
}
-1
View File
@@ -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();
}); });
+7 -2
View File
@@ -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"`
} }
+80 -34
View File
@@ -13,12 +13,12 @@ import (
func Info(w http.ResponseWriter, r *http.Request) { func Info(w http.ResponseWriter, r *http.Request) {
info := map[string]any{ info := map[string]any{
"version": base.Version, "version": base.Version,
"runtime": runtime.Version(), "runtime": runtime.Version(),
"os": runtime.GOOS, "os": runtime.GOOS,
"arch": runtime.GOARCH, "arch": runtime.GOARCH,
"inDocker": base.InDocker == "true",
} }
info["inDocker"] = base.InDocker == "true"
WriteJson(w, model.NewOkResult(info)) WriteJson(w, model.NewOkResult(info))
} }
@@ -74,13 +74,27 @@ func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
} }
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
} }
@@ -88,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
} }
@@ -118,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
} }
@@ -133,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
} }
@@ -159,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))
} }
@@ -324,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 {
+9 -6
View File
@@ -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
+21 -10
View File
@@ -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")
@@ -93,9 +104,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
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.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)
@@ -143,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))
} }
}() }()
+3 -11
View File
@@ -140,7 +140,7 @@ func TestCreateDirectTask(t *testing.T) {
func TestCreateDirectTaskBatch(t *testing.T) { func TestCreateDirectTaskBatch(t *testing.T) {
doTest(func() { doTest(func() {
reqs := make([]*base.Request, 0) reqs := make([]*base.Request, 0)
for i := 0; i < 6; i++ { for i := 0; i < 5; i++ {
reqs = append(reqs, createReq.Req) reqs = append(reqs, createReq.Req)
} }
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{ taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
@@ -582,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
+14
View File
@@ -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
}
+60
View File
@@ -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)
}
})
}
}
+1 -7
View File
@@ -8,18 +8,13 @@ import (
"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)
} }
@@ -32,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
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -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.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 63 KiB

@@ -41,9 +41,8 @@ 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,
}); });
@@ -55,10 +54,19 @@ class HttpConfig {
@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);
@@ -71,6 +79,7 @@ class ExtraConfig {
String themeMode; String themeMode;
String locale; String locale;
bool lastDeleteTaskKeep; bool lastDeleteTaskKeep;
bool defaultDirectDownload;
ExtraConfigBt bt = ExtraConfigBt(); ExtraConfigBt bt = ExtraConfigBt();
@@ -78,6 +87,7 @@ class ExtraConfig {
this.themeMode = '', this.themeMode = '',
this.locale = '', this.locale = '',
this.lastDeleteTaskKeep = false, this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false,
}); });
factory ExtraConfig.fromJson(Map<String, dynamic>? json) => factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
@@ -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,20 +49,30 @@ 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? ?? '',
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false, lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>); )..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) => Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
@@ -71,6 +80,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'themeMode': instance.themeMode, 'themeMode': instance.themeMode,
'locale': instance.locale, 'locale': instance.locale,
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep, 'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
'defaultDirectDownload': instance.defaultDirectDownload,
'bt': instance.bt.toJson(), 'bt': instance.bt.toJson(),
}; };
+25
View File
@@ -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);
}
+28
View File
@@ -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',
};
+2 -2
View File
@@ -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>),
+11
View File
@@ -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) =>
+36 -12
View File
@@ -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,
}; };
@@ -17,6 +17,7 @@ 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 '../../../../database/entity.dart';
import '../../../../i18n/message.dart'; import '../../../../i18n/message.dart';
@@ -110,6 +111,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
void onClose() { void onClose() {
_linkSubscription?.cancel(); _linkSubscription?.cancel();
trayManager.removeListener(this); trayManager.removeListener(this);
LibgopeedBoot.instance.stop();
} }
@override @override
@@ -193,6 +195,9 @@ 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');
} }
@@ -240,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()) {
@@ -258,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,
@@ -287,6 +295,11 @@ 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',
),
); );
} }
} }
@@ -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
@@ -32,6 +32,10 @@ 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();
@@ -39,6 +43,8 @@ class CreateView extends GetView<CreateController> {
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
@@ -206,7 +212,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),
@@ -261,7 +267,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 [
@@ -293,6 +456,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(
@@ -301,7 +480,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,
)), )),
], ],
@@ -396,7 +575,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
@@ -416,18 +595,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);
} }
@@ -440,20 +627,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;
@@ -536,7 +742,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,
@@ -5,6 +5,7 @@ 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:launch_at_startup/launch_at_startup.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -108,6 +109,28 @@ 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),
@@ -334,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(
@@ -779,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: () {
@@ -813,6 +925,7 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([ children: _addDivider([
buildDownloadDir(), buildDownloadDir(),
buildMaxRunning(), buildMaxRunning(),
buildDefaultDirectDownload(),
buildBrowserExtension(), buildBrowserExtension(),
buildAutoStartup(), buildAutoStartup(),
]), ]),
@@ -833,6 +946,7 @@ class SettingView extends GetView<SettingController> {
buildBtListenPort(), buildBtListenPort(),
buildBtTrackerSubscribeUrls(), buildBtTrackerSubscribeUrls(),
buildBtTrackers(), buildBtTrackers(),
buildBtSeedConfig(),
]), ]),
)), )),
Text('ui'.tr), Text('ui'.tr),
@@ -889,6 +1003,13 @@ class SettingView extends GetView<SettingController> {
: null, : null,
]), ]),
)), )),
Text('developer'.tr),
Card(
child: Column(
children: _addDivider([
buildLogsDir(),
]),
)),
]), ]),
)) ))
], ],
@@ -1,5 +1,10 @@
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);
final copyUrlDone = false.obs;
} }
@@ -104,7 +104,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
}() }()
: [ : [
IconButton( IconButton(
icon: const Icon(Icons.play_circle), icon: const Icon(Icons.open_in_new),
onPressed: () async { onPressed: () async {
await OpenFilex.open(filePath); await OpenFilex.open(filePath);
}), }),
@@ -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,18 +1,16 @@
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 '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/file_icon.dart';
import '../../util/icons.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/app/controllers/app_controller.dart';
import '../modules/task/controllers/task_controller.dart';
import '../modules/task/views/task_view.dart';
import '../routes/app_pages.dart'; import '../routes/app_pages.dart';
class BuildTaskListView extends GetView { class BuildTaskListView extends GetView {
@@ -60,17 +58,7 @@ class BuildTaskListView extends GetView {
} }
bool isFolderTask() { bool isFolderTask() {
return task.meta.res?.name.isNotEmpty ?? false; return task.isFolder;
}
String buildExplorerUrl(Task task) {
if (!isFolderTask()) {
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) { Future<void> showDeleteDialog(String id) {
@@ -119,22 +107,13 @@ class BuildTaskListView extends GetView {
)); ));
} }
toTaskFilesView() {
if (Util.isDesktop()) {
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
} else {
Get.rootDelegate
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
}
}
List<Widget> buildActions() { List<Widget> buildActions() {
final list = <Widget>[]; final list = <Widget>[];
if (isDone()) { if (isDone()) {
list.add(IconButton( list.add(IconButton(
icon: const Icon(Icons.folder_open), icon: const Icon(Icons.folder_open),
onPressed: () { onPressed: () {
toTaskFilesView(); task.explorer();
}, },
)); ));
} else { } else {
@@ -188,27 +167,26 @@ class BuildTaskListView extends GetView {
(total > 0 ? " / ${Util.fmtByte(total)}" : ""); (total > 0 ? " / ${Util.fmtByte(total)}" : "");
} }
final taskController = Get.find<TaskController>();
return Card( return Card(
elevation: 4.0, elevation: 4.0,
child: InkWell( child: InkWell(
onTap: () {}, onTap: () {
taskController.scaffoldKey.currentState?.openEndDrawer();
taskController.selectTask.value = task;
},
onDoubleTap: () { onDoubleTap: () {
if (isDone()) { task.open();
if (isFolderTask()) {
toTaskFilesView();
} else {
OpenFilex.open(buildExplorerUrl(task));
}
}
}, },
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( ListTile(
title: Text(fileName(task.meta)), title: Text(task.name),
leading: isFolderTask() leading: isFolderTask()
? const Icon(FaIcons.folder) ? const Icon(FaIcons.folder)
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))])), : Icon(FaIcons.allIcons[findIcon(task.name)])),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -239,29 +217,4 @@ class BuildTaskListView extends GetView {
), ),
)).padding(horizontal: 14, top: 8); )).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;
}
} }
+44
View File
@@ -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,20 @@
import 'dart:io'; 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: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:permission_handler/permission_handler.dart';
import 'package:toggle_switch/toggle_switch.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/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;
@@ -68,12 +72,26 @@ class _DirectorySelectorState extends State<DirectorySelector> {
if (index == 0) { if (index == 0) {
return false; return false;
} }
// Check write permission
final downloadDir = final downloadDir =
(await DownloadsPath.downloadsDirectory())?.path; (await DownloadsPath.downloadsDirectory())?.path;
if (downloadDir == null) { if (downloadDir == null) {
return true; 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 = final fileRandomeName =
"test_${DateTime.now().millisecondsSinceEpoch}.tmp"; "test_${DateTime.now().millisecondsSinceEpoch}.tmp";
final testFile = File('$downloadDir/Gopeed/$fileRandomeName'); final testFile = File('$downloadDir/Gopeed/$fileRandomeName');
@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import '../../util/util.dart'; import '../../util/util.dart';
import '../common/libgopeed_channel.dart'; import '../common/libgopeed_channel.dart';
@@ -17,7 +16,19 @@ class LibgopeedBootNative implements LibgopeedBoot {
late LibgopeedInterface _libgopeed; late LibgopeedInterface _libgopeed;
LibgopeedBootNative() { LibgopeedBootNative() {
if (!Util.isDesktop()) { if (Util.isDesktop()) {
var libName = "libgopeed.";
if (Platform.isWindows) {
libName += "dll";
}
if (Platform.isMacOS) {
libName += "dylib";
}
if (Platform.isLinux) {
libName += "so";
}
_libgopeed = LibgopeedFFi(LibgopeedBind(DynamicLibrary.open(libName)));
} else {
_libgopeed = LibgopeedChannel(); _libgopeed = LibgopeedChannel();
} }
} }
@@ -27,8 +38,7 @@ class LibgopeedBootNative implements LibgopeedBoot {
cfg.storage = 'bolt'; cfg.storage = 'bolt';
cfg.storageDir = Util.getStorageDir(); cfg.storageDir = Util.getStorageDir();
cfg.refreshInterval = 0; cfg.refreshInterval = 0;
var port = var port = await _libgopeed.start(cfg);
Util.isDesktop() ? await _ffiStart(cfg) : await _libgopeed.start(cfg);
return port; return port;
} }
@@ -36,26 +46,4 @@ class LibgopeedBootNative implements LibgopeedBoot {
Future<void> stop() async { Future<void> stop() async {
await _libgopeed.stop(); await _libgopeed.stop();
} }
_ffiInit() {
var libName = "libgopeed.";
if (Platform.isWindows) {
libName += "dll";
}
if (Platform.isMacOS) {
libName += "dylib";
}
if (Platform.isLinux) {
libName += "so";
}
_libgopeed = LibgopeedFFi(LibgopeedBind(DynamicLibrary.open(libName)));
}
// FFI run in isolate
Future<int> _ffiStart(StartConfig cfg) async {
return await Isolate.run(() async {
_ffiInit();
return await _libgopeed.start(cfg);
});
}
} }
-9
View File
@@ -54,15 +54,6 @@ class Database {
_startConfig, (json) => StartConfigEntity.fromJson(json)); _startConfig, (json) => StartConfigEntity.fromJson(json));
} }
void saveLastRunningConfig(StartConfigEntity entity) {
save<StartConfigEntity>(_lastRunningConfig, entity);
}
StartConfigEntity? getLastRunningConfig() {
return get<StartConfigEntity>(
_lastRunningConfig, (json) => StartConfigEntity.fromJson(json));
}
/// Patch non-null fields with the original value /// Patch non-null fields with the original value
void saveWindowState(WindowStateEntity entity) { void saveWindowState(WindowStateEntity entity) {
final state = getWindowState(); final state = getWindowState();
+13
View File
@@ -17,6 +17,7 @@ const enUS = {
'create': 'Create Task', 'create': 'Create Task',
'directDownload': 'Direct Download', 'directDownload': 'Direct Download',
'advancedOptions': 'Advanced Options', 'advancedOptions': 'Advanced Options',
'followSettings': 'Follow Settings',
'downloadLink': 'Download Link', 'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link', 'downloadLinkValid': 'Please enter the download link',
'downloadLinkHit': 'downloadLinkHit':
@@ -35,6 +36,7 @@ const enUS = {
'connections': 'Connections', 'connections': 'Connections',
'useServerCtime': 'Use server time for file creation', 'useServerCtime': 'Use server time for file creation',
'maxRunning': 'Max Running Tasks', 'maxRunning': 'Max Running Tasks',
'defaultDirectDownload': 'Check direct download by default',
'items': '@count items', 'items': '@count items',
'subscribeTracker': 'Subscribe Tracker', 'subscribeTracker': 'Subscribe Tracker',
'subscribeFail': 'subscribeFail':
@@ -60,6 +62,8 @@ const enUS = {
'set': 'SET', 'set': 'SET',
'portInUse': 'Port [@port] is in use, please change the port', 'portInUse': 'Port [@port] is in use, please change the port',
'effectAfterRestart': 'Effect after restart', 'effectAfterRestart': 'Effect after restart',
'developer': 'Developer',
'logDirectory': 'Log Directory',
'show': 'Show', 'show': 'Show',
'startAll': 'Start All', 'startAll': 'Start All',
'pauseAll': 'Pause All', 'pauseAll': 'Pause All',
@@ -95,5 +99,14 @@ const enUS = {
'Thanks to all the contributors who have helped build and develop the Gopeed community!', 'Thanks to all the contributors who have helped build and develop the Gopeed community!',
'browserExtension': 'Browser Extension', 'browserExtension': 'Browser Extension',
'launchAtStartup': 'Launch at Startup', 'launchAtStartup': 'Launch at Startup',
'seedConfig': 'Seed Config',
'seedKeep': 'Keep seeding until manually stopped',
'seedRatio': 'Seed ratio',
'seedTime': 'Seed time (minutes)',
'taskDetail': 'Task Detail',
'taskName': 'Task Name',
'taskUrl': 'Task URL',
'downloadPath': 'Download Path',
'skipVerifyCert': 'Skip Certificate Verification',
}, },
}; };
+108
View File
@@ -0,0 +1,108 @@
const esES = {
'es_ES': {
'label': 'Español',
'error': 'Error',
'tip': 'Consejo',
'confirm': 'Confirmar',
'cancel': 'Cancelar',
'on': 'Activado',
'off': 'Desactivado',
'selectAll': 'Seleccionar Todo',
'task': 'Tareas',
'downloading': 'descargando',
'downloaded': 'descargado',
'setting': 'Ajustes',
'donate': 'Donar',
'exit': 'Salir',
'create': 'Crear Tarea',
'directDownload': 'Descarga Directa',
'advancedOptions': 'Opciones Avanzadas',
'downloadLink': 'Enlace de Descarga',
'downloadLinkValid': 'Por favor, introduce el enlace de descarga',
'downloadLinkHit':
'Por favor, introduce el enlace de descarga, se admite HTTP/HTTPS/MAGNET@append',
'downloadLinkHitDesktop': ', o arrastra el archivo torrent aquí directamente',
'download': 'Descargar',
'noFileSelected': 'Por favor, selecciona al menos un archivo para continuar.',
'noStoragePermission': 'Se requiere permiso de almacenamiento',
'selectFile': 'Seleccionar Archivo',
'rename': 'Renombrar',
'basic': 'Básico',
'advanced': 'Avanzado',
'general': 'General',
'downloadDir': 'Directorio de Descarga',
'downloadDirValid': 'Por favor, selecciona el directorio de descarga',
'connections': 'Conexiones',
'useServerCtime': 'Usar tiempo del servidor para la creación de archivos',
'maxRunning': 'Máximo de Tareas en Ejecución',
'items': '@count elementos',
'subscribeTracker': 'Suscribirse al Tracker',
'subscribeFail':
'Suscripción fallida, por favor verifica la red o intenta más tarde',
'update': 'Actualizar',
'updateDaily': 'Actualizar diariamente',
'lastUpdate': 'Última actualización: @time',
'addTracker': 'Añadir Tracker',
'addTrackerHit': 'Por favor, introduce la URL del servidor tracker, una por línea',
'ui': 'Interfaz',
'theme': 'Tema',
'themeSystem': 'Sistema',
'themeLight': 'Claro',
'themeDark': 'Oscuro',
'locale': 'Idioma',
'about': 'Acerca de',
'homepage': 'Página de inicio',
'version': 'Versión',
'protocol': 'Protocolo',
'port': 'Puerto',
'apiToken': 'Token API',
'notSet': 'No establecido',
'set': 'Establecido',
'portInUse': 'El puerto [@port] está en uso, por favor cambia el puerto',
'effectAfterRestart': 'Efectivo después de reiniciar',
'show': 'Mostrar',
'startAll': 'Iniciar Todo',
'pauseAll': 'Pausar Todo',
'deleteTask': 'Eliminar Tarea',
'deleteTaskTip': 'Mantener archivos descargados',
'delete': 'Eliminar',
'newVersionTitle': 'Nueva versión @version disponible',
'newVersionUpdate': 'Actualizar Ahora',
'newVersionLater': 'Más tarde',
'extensions': 'Extensiones',
'extensionInstallUrl': 'URL de instalación',
'extensionInstallSuccess': 'Instalado con éxito',
'extensionUpdateSuccess': 'Actualizado con éxito',
'extensionDelete': 'Eliminar Extensión',
'extensionAlreadyLatest': 'Ya es la versión más reciente',
'extensionFind': 'Buscar Extensiones',
'extensionDevelop': 'Desarrollar Extensiones',
'history': 'Historial',
'clearHistory': 'Borrar Historial',
'noHistoryFound': 'No se encontró historial',
'serviceTitle': 'Servicio de Descarga',
'serviceText': 'En ejecución',
'network': 'Red',
'proxy': 'Proxy',
'noProxy': 'Sin Proxy',
'systemProxy': 'Proxy del Sistema',
'customProxy': 'Proxy Personalizado',
'server': 'Servidor',
'username': 'Nombre de usuario',
'password': 'Contraseña',
'thanks': 'Agradecimientos',
'thanksDesc':
'¡Gracias a todos los contribuyentes que han ayudado a construir y desarrollar la comunidad Gopeed!',
'browserExtension': 'Extensión del Navegador',
'launchAtStartup': 'Iniciar al Arranque',
'seedConfig': 'Configuración de Semilla',
'seedKeep': 'Mantener sembrado',
'seedRatio': 'Ratio de semilla',
'seedTime': 'Tiempo de semilla (minutos)',
'taskDetail': 'Detalles de la Tarea',
'taskName': 'Nombre de la Tarea',
'taskUrl': 'URL de la Tarea',
'downloadPath': 'Ruta de Descarga',
'skipVerifyCert': 'Omitir Verificación de Certificado',
},
};
+1
View File
@@ -61,5 +61,6 @@ const faIR = {
'thanksDesc': 'thanksDesc':
'از تمامی افرادی که در ساخت و توسعه‌ی جامعه‌ی Gopeed مشارکت کرده‌اند، سپاسگزاری می‌کنیم!', 'از تمامی افرادی که در ساخت و توسعه‌ی جامعه‌ی Gopeed مشارکت کرده‌اند، سپاسگزاری می‌کنیم!',
'browserExtension': 'فایل های دانلود شده را نگه دارد', 'browserExtension': 'فایل های دانلود شده را نگه دارد',
'skipVerifyCert': 'رد کردن تأیید گواهینامه',
} }
}; };
+97
View File
@@ -0,0 +1,97 @@
const frFR = {
'fr_FR': {
'label': 'Français',
'error': 'Erreur',
'tip': 'Conseil',
'confirm': 'Confirmer',
'cancel': 'Annuler',
'on': 'Activé',
'off': 'Désactivé',
'selectAll': 'Sélectionner tout',
'task': 'Tâches',
'downloading': 'Téléchargement',
'downloaded': 'Téléchargé',
'setting': 'Paramètres',
'donate': 'Faire un don',
'exit': 'Quitter',
'create': 'Créer une tâche',
'directDownload': 'Téléchargement direct',
'advancedOptions': 'Options avancées',
'downloadLink': 'Lien de téléchargement',
'downloadLinkValid': 'Veuillez entrer le lien de téléchargement',
'downloadLinkHit': 'Veuillez entrer le lien de téléchargement, HTTP/HTTPS/MAGNET pris en charge',
'downloadLinkHitDesktop': ', ou glissez directement le fichier torrent ici',
'download': 'Télécharger',
'noFileSelected': 'Veuillez sélectionner au moins un fichier pour continuer.',
'noStoragePermission': 'Permission de stockage requise',
'selectFile': 'Sélectionner un fichier',
'rename': 'Renommer',
'basic': 'De base',
'advanced': 'Avancé',
'general': 'Général',
'downloadDir': 'Répertoire de téléchargement',
'downloadDirValid': 'Veuillez sélectionner le répertoire de téléchargement',
'connections': 'Connexions',
'useServerCtime': 'Utiliser l\'heure du serveur pour la création de fichier',
'maxRunning': 'Tâches en cours maximum',
'items': '@count éléments',
'subscribeTracker': 'S\'abonner au tracker',
'subscribeFail': 'Abonnement échoué, veuillez vérifier le réseau ou réessayer plus tard',
'update': 'Mettre à jour',
'updateDaily': 'Mettre à jour quotidiennement',
'lastUpdate': 'Dernière mise à jour : @time',
'addTracker': 'Ajouter un tracker',
'addTrackerHit': 'Veuillez entrer l\'URL du serveur de tracker, une par ligne',
'ui': 'Interface utilisateur',
'theme': 'Thème',
'themeSystem': 'Système',
'themeLight': 'Clair',
'themeDark': 'Sombre',
'locale': 'Langue',
'about': 'À propos',
'homepage': 'Page d\'accueil',
'version': 'Version',
'protocol': 'Protocole',
'port': 'Port',
'apiToken': 'Jeton API',
'notSet': 'Non défini',
'set': 'Défini',
'portInUse': 'Le port [@port] est utilisé, veuillez changer de port',
'effectAfterRestart': 'Effectif après redémarrage',
'show': 'Afficher',
'startAll': 'Tout démarrer',
'pauseAll': 'Tout suspendre',
'deleteTask': 'Supprimer la tâche',
'deleteTaskTip': 'Conserver les fichiers téléchargés',
'delete': 'Supprimer',
'newVersionTitle': 'Nouvelle version @version disponible',
'newVersionUpdate': 'Mettre à jour maintenant',
'newVersionLater': 'Plus tard',
'extensions': 'Extensions',
'extensionInstallUrl': 'URL d\'installation',
'extensionInstallSuccess': 'Installé avec succès',
'extensionUpdateSuccess': 'Mis à jour avec succès',
'extensionDelete': 'Supprimer l\'extension',
'extensionAlreadyLatest': 'C\'est déjà la dernière version',
'extensionFind': 'Trouver des extensions',
'extensionDevelop': 'Développer des extensions',
'history': 'Historique',
'clearHistory': 'Effacer l\'historique',
'noHistoryFound': 'Aucun historique trouvé',
'serviceTitle': 'Service de téléchargement',
'serviceText': 'En cours d\'exécution',
'network': 'Réseau',
'proxy': 'Proxy',
'noProxy': 'Pas de proxy',
'systemProxy': 'Proxy système',
'customProxy': 'Proxy personnalisé',
'server': 'Serveur',
'username': 'Nom d\'utilisateur',
'password': 'Mot de passe',
'thanks': 'Merci',
'thanksDesc': 'Merci à tous les contributeurs qui ont aidé à construire et développer la communauté Gopeed !',
'browserExtension': 'Extension du navigateur',
'launchAtStartup': 'Lancer au démarrage',
'skipVerifyCert': 'Bypass Verifikasi Sertifikat',
},
};
+112
View File
@@ -0,0 +1,112 @@
const idID = {
'id_ID': {
'label': 'Indonesia',
'error': 'Kesalahan',
'tip': 'Tips',
'confirm': 'Konfirmasi',
'cancel': 'Batal',
'on': 'Aktif',
'off': 'Nonaktif',
'selectAll': 'Pilih Semua',
'task': 'Tugas',
'downloading': 'Mengunduh',
'downloaded': 'Terunduh',
'setting': 'Pengaturan',
'donate': 'Donasi',
'exit': 'Keluar',
'create': 'Membuat Tugas',
'directDownload': 'Unduhan Langsung',
'advancedOptions': 'Pengaturan Lanjutan',
'followSettings': 'Mengikuti Pengaturan',
'downloadLink': 'Tautan Unduhan',
'downloadLinkValid': 'Silakan masukkan tautan unduhan',
'downloadLinkHit':
'Tolong masukkan tautan unduhan, HTTP/HTTPS/MAGNET didukung@append',
'downloadLinkHitDesktop': ', atau seret file torrent ke sini secara langsung',
'download': 'Download',
'noFileSelected': 'Silakan pilih setidaknya satu file untuk melanjutkan.',
'noStoragePermission': 'Izin penyimpanan diperlukan',
'selectFile': 'Pilih File',
'rename': 'Mengganti Nama',
'basic': 'Dasar',
'advanced': 'Lanjutan',
'general': 'Umum',
'downloadDir': 'Direktori Unduhan',
'downloadDirValid': 'Silahkan pilih direktori unduhan',
'connections': 'Koneksi',
'useServerCtime': 'Gunakan waktu server untuk pembuatan file',
'maxRunning': 'Tugas Maksimum yang Berjalan',
'defaultDirectDownload': 'Pilih unduhan langsung sebagai opsi standar',
'items': '@count items',
'subscribeTracker': 'Berlangganan Tracker',
'subscribeFail':
'Berlangganan gagal, periksa jaringan atau coba lagi nanti',
'update': 'Perbarui',
'updateDaily': 'Perbarui setiap hari',
'lastUpdate': 'Pembaruan terakhir: @time',
'addTracker': 'Tambah Tracker',
'addTrackerHit': 'Silakan masukkan URL server tracker, satu per baris',
'ui': 'UI',
'theme': 'Tema',
'themeSystem': 'Sistem',
'themeLight': 'Terang',
'themeDark': 'Gelap',
'locale': 'Bahasa',
'about': 'Tentang',
'homepage': 'Beranda',
'version': 'Versi',
'protocol': 'Protokol',
'port': 'Port',
'apiToken': 'Token API',
'notSet': 'Tidak Diatur',
'set': 'ATUR',
'portInUse': 'Port [@port] sedang digunakan, silakan ubah port',
'effectAfterRestart': 'Berlaku setelah restart',
'developer': 'Pengembang',
'logDirectory': 'Direktori Log',
'show': 'Tampilkan',
'startAll': 'Mulai Semua',
'pauseAll': 'Jeda Semua',
'deleteTask': 'Hapus Tugas',
'deleteTaskTip': 'Simpan file yang terunduh',
'delete': 'Hapus',
'newVersionTitle': 'Temukan versi batu @version',
'newVersionUpdate': 'Perbarui Sekarang',
'newVersionLater': 'Nanti',
'extensions': 'Ekstensi',
'extensionInstallUrl': 'URL Instal',
'extensionInstallSuccess': 'Diinstal dengan sukses',
'extensionUpdateSuccess': 'Diperbaruhi dengan sukses',
'extensionDelete': 'Hapus Ekstensi',
'extensionAlreadyLatest': 'Ini sudah versi terbaru',
'extensionFind': 'Temukan Ekstensi',
'extensionDevelop': 'Kembangkan Ekstensi',
'history': 'Riwayat',
'clearHistory': 'Hapus Riwayat',
'noHistoryFound': 'Riwayat Tidak DItemukan',
'serviceTitle': 'Layanan Unduhan',
'serviceText': 'Sedang Berjalan',
'network': 'Jaringan',
'proxy': 'Proxy',
'noProxy': 'Tanpa Proxy',
'systemProxy': 'Proxy Sistem',
'customProxy': 'Proxy Kustom',
'server': 'Server',
'username': 'Nama Pengguna',
'password': 'Kata Sandi',
'thanks': 'Terima Kasih',
'thanksDesc':
'Terima kasih kepada semua kontributor yang telah membantu membangun dan mengembangkan komunitas Gopeed!',
'browserExtension': 'Ekstensi Perambanan',
'launchAtStartup': 'Luncurkan saat Startup',
'seedConfig': 'Konfigurasi Seed',
'seedKeep': 'Terus seeding hingga dihentikan secara manual',
'seedRatio': 'Rasio Seed',
'seedTime': 'Waktu Seed (menit)',
'taskDetail': 'Detail Tugas',
'taskName': 'Nama Tugas',
'taskUrl': 'URL Tugas',
'downloadPath': 'Direktori Unduhan',
'skipVerifyCert': 'Lupakan verifikasi sertifikat',
},
};
+101
View File
@@ -0,0 +1,101 @@
const itIT = {
'it_IT': {
'label': 'Italiano',
'error': 'Errore',
'tip': 'Suggerimento',
'confirm': 'Conferma',
'cancel': 'Annullla',
'on': 'Acceso',
'off': 'Spento',
'selectAll': 'Seleziona tutto',
'task': 'Attività',
'downloading': 'scaricamento in corso',
'downloaded': 'scaricato',
'setting': 'Impostazioni',
'donate': 'Donazione',
'exit': 'Uscita',
'create': 'Crea attività',
'directDownload': 'Scaricamento diretto',
'advancedOptions': 'Opzioni avanzate',
'downloadLink': 'Link di scaricamento',
'downloadLinkValid': 'Inserisci il collegamento per lo scaricamento',
'downloadLinkHit':
'Inserisci il collegamento per il download, HTTP/HTTPS/MAGNET supported@append',
'downloadLinkHitDesktop':
', oppure trascina qui direttamente il file torrent',
'download': 'Download',
'noFileSelected': 'Seleziona almeno un file per continuare.',
'noStoragePermission': "È richiesta l'autorizzazione di archiviazione",
'selectFile': 'Selezione file',
'rename': 'Rinomina',
'basic': 'Basico',
'advanced': 'Avanzato',
'general': 'Generale',
'downloadDir': 'Directory di scaricamento',
'downloadDirValid': 'Seleziona la directory di scaricamento',
'connections': 'Connessioni',
'useServerCtime': "Utilizza l'ora del server per la creazione dei file",
'maxRunning': 'Numero massimo di attività in esecuzione',
'items': '@count elementi',
'subscribeTracker': 'Iscriviti al tracker',
'subscribeFail':
'Iscrizione non riuscita, controlla la rete o riprova più tardir',
'update': 'Aggiornaento',
'updateDaily': 'Aggiornamento quotidiano',
'lastUpdate': 'Ultimo aggiornamento: @time',
'addTracker': 'Aggiungi Tracker',
'addTrackerHit': "Inserisci l'URL del server del tracker, uno per riga",
'ui': 'UI',
'theme': 'Tema',
'themeSystem': 'Sistema',
'themeLight': 'Chiaro',
'themeDark': 'Scuro',
'locale': 'Lingua',
'about': 'Informazioni',
'homepage': 'Homepage',
'version': 'Versione',
'protocol': 'Protocollo',
'port': 'Porta',
'apiToken': 'Token API',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'La porta [@port] è in uso, per favore cambia la porta',
'effectAfterRestart': 'Effetto dopo il riavvio',
'show': 'Mostra',
'startAll': 'Avvia tutti',
'pauseAll': 'Mtti in pausa tutti',
'deleteTask': 'Elimina attività',
'deleteTaskTip': 'Conserva i file scaricati',
'delete': 'Elimina',
'newVersionTitle': 'Scopri la nuova versione @version',
'newVersionUpdate': 'Aggiorna ora',
'newVersionLater': 'Dopo',
'extensions': 'Estensioni',
'extensionInstallUrl': 'Installa URL',
'extensionInstallSuccess': 'Installato con successo',
'extensionUpdateSuccess': 'Aggiornato con successo',
'extensionDelete': 'Elimina estensione',
'extensionAlreadyLatest': "È già l'ultima versione",
'extensionFind': 'Trova estensioni',
'extensionDevelop': 'Sviluppare estensioni',
'history': 'Cronologia',
'clearHistory': 'Pulisci cronologia',
'noHistoryFound': 'Nessuna cronologia trovata',
'serviceTitle': 'Scarica il servizio',
'serviceText': 'In esecuzione',
'network': 'Rete',
'proxy': 'Proxy',
'noProxy': 'Nessun Proxy',
'systemProxy': 'Sistema Proxy',
'customProxy': 'Proxy personalizzato',
'server': 'Server',
'username': 'Nome utente',
'password': 'Password',
'thanks': 'Ringraziamenti',
'thanksDesc':
'Grazie a tutti i contributori che hanno contribuito a costruire e sviluppare la comunità Gopeed!',
'browserExtension': 'Estensione del browser',
'launchAtStartup': "Lancia all'avvio",
'skipVerifyCert': 'Salta la verifica del certificato',
},
};
+1
View File
@@ -62,5 +62,6 @@ const jaJP = {
'thanks': '感謝', 'thanks': '感謝',
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!', 'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
'browserExtension': 'ブラウザ拡張機能', 'browserExtension': 'ブラウザ拡張機能',
'skipVerifyCert': '証明書の検証をスキップ',
} }
}; };
+4 -3
View File
@@ -20,10 +20,10 @@ const plPL = {
'downloadLink': 'Pobierz z linku', 'downloadLink': 'Pobierz z linku',
'downloadLinkValid': 'Otwórz link pobierania', 'downloadLinkValid': 'Otwórz link pobierania',
'downloadLinkHit': 'downloadLinkHit':
'Otwórz link pobierania, HTTP/HTTPS/MAGNET supported@append', 'Otwórz link pobierania, HTTP/HTTPS/MAGNET wspierane@append',
'downloadLinkHitDesktop': ', lub przenieś bezpośrednio plik torrent', 'downloadLinkHitDesktop': ', lub przenieś bezpośrednio plik torrent',
'download': 'Pobiers', 'download': 'Pobierz',
'noFileSelected': 'Wybierz conajmniej jeden plik aby kontynuować.', 'noFileSelected': 'Wybierz co najmniej jeden plik aby kontynuować.',
'noStoragePermission': 'Przyznaj uprawnienia do zapisu', 'noStoragePermission': 'Przyznaj uprawnienia do zapisu',
'selectFile': 'Wybierz plik', 'selectFile': 'Wybierz plik',
'rename': 'Zmień nazwę', 'rename': 'Zmień nazwę',
@@ -94,5 +94,6 @@ const plPL = {
'Dziękuję wszystkim za pomoc przy powstawaniu aplikacji Gopeed!', 'Dziękuję wszystkim za pomoc przy powstawaniu aplikacji Gopeed!',
'browserExtension': 'Wtyczka dla przeglądarki', 'browserExtension': 'Wtyczka dla przeglądarki',
'launchAtStartup': 'Uruchom na starcie', 'launchAtStartup': 'Uruchom na starcie',
'skipVerifyCert': 'Pomiń weryfikację certyfikatu',
}, },
}; };
+1
View File
@@ -77,5 +77,6 @@ const ruRU = {
'thanksDesc': 'thanksDesc':
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!', 'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
'browserExtension': 'Расширение браузера', 'browserExtension': 'Расширение браузера',
'skipVerifyCert': 'Пропустить проверку сертификата',
} }
}; };
+100
View File
@@ -0,0 +1,100 @@
const taTA = {
'ta_TA': {
'label': 'தமிழ்',
'error': 'பிழை',
'tip': 'குறிப்பு',
'confirm': 'சரி',
'cancel': 'ரத்துசெய்',
'on': 'On',
'off': 'Off',
'selectAll': 'அனைத்தையும் தேர்வுசெய்',
'task': 'பணிகள்',
'downloading': 'பதிவிறக்குகிறது',
'downloaded': 'பதிவிறக்கம் செய்யப்பட்டது',
'setting': 'அமைப்புகள் ',
'donate': 'நன்கொடை',
'exit': 'வெளியேறு',
'create': 'பணியை உருவாக்கு',
'directDownload': 'நேரடி பதிவிறக்கம்',
'advancedOptions': 'மேம்பட்ட விருப்பங்கள்',
'downloadLink': 'தரவிறக்க இணைப்பு',
'downloadLinkValid': 'பதிவிறக்க இணைப்பை உள்ளிடவும்',
'downloadLinkHit':
'பதிவிறக்க இணைப்பை உள்ளிடவும், HTTP/HTTPS/MAGNET ஆகியவற்றை ஏற்கும்@append',
'downloadLinkHitDesktop': ', அல்லது torrent கோப்பை நேரடியாக இங்கே இழுக்கவும்',
'download': 'பதிவிறக்கு',
'noFileSelected': 'குறைந்தபட்சம் ஒரு கோப்பைத் தேர்ந்தெடுக்கவும்.',
'noStoragePermission': 'சேமிப்பக அனுமதி தேவை',
'selectFile': 'கோப்பைத் தேர்ந்தெடுக்கவும்',
'rename': 'பெயறைமாற்று',
'basic': 'அடிப்படை',
'advanced': 'மேம்பட்ட',
'general': 'பொதுவான',
'downloadDir': 'பதிவிரக்குமிடம்',
'downloadDirValid': 'பதிவிரக்குமிடத்தை தேர்வு செயுங்கள்',
'connections': 'தொடர்புகள்',
'useServerCtime': 'Use server time for file creation',
'maxRunning': 'அதிகபட்ச இயங்கும் பணிகள்',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
'Subscribe failed, please check network or try again later',
'update': 'புதுப்பிக்கவும்',
'updateDaily': 'தினமும் புதுப்பிக்கவும்',
'lastUpdate': 'கடைசியாக புதுப்பிக்கப்பட்டது: @time',
'addTracker': 'Trackerஐ சேர்',
'addTrackerHit': 'Please enter the tracker server url, one per line',
'ui': 'UI',
'theme': 'தீம்',
'themeSystem': 'இயல்புநிலை',
'themeLight': 'வெண்மையானநிலை',
'themeDark': 'இருண்டநிலை',
'locale': 'மொழி',
'about': 'எங்களை பற்றி',
'homepage': 'முகப்புப்பக்கம்',
'version': 'பதிப்பு',
'protocol': 'நெறிமுறை',
'port': 'Port',
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'Port [@port] is in use, please change the port',
'effectAfterRestart': 'Effect after restart',
'show': 'காட்டு',
'startAll': 'அனைத்தையும் தொடங்கு',
'pauseAll': 'அனைத்தையும் நிறுத்திவை',
'deleteTask': 'பணியை நீக்கு',
'deleteTaskTip': 'பதிவிறக்கம் செய்யப்பட்ட கோப்புகளை வைத்திரு',
'delete': 'நீக்கு',
'newVersionTitle': 'புதிய பதிப்பைக் கண்டறியவும் @version',
'newVersionUpdate': 'இப்பொழுது புதுப்பி',
'newVersionLater': 'பின்னர்',
'extensions': 'நீட்டிப்புகள்',
'extensionInstallUrl': 'நிறுவப்படும் URL',
'extensionInstallSuccess': 'வெற்றிகரமாக நிறுவப்பட்டது',
'extensionUpdateSuccess': 'வெற்றிகரமாக புதுப்பிக்கப்பட்டது',
'extensionDelete': 'நீட்டிப்பை நீக்கு',
'extensionAlreadyLatest': 'இது ஏற்கனவே சமீபத்திய பதிப்பாகும்',
'extensionFind': 'நீட்டிப்புகளைக் கண்டறியவும்',
'extensionDevelop': 'நீட்டிப்புகளை உருவாக்கவும்',
'history': 'வரலாறு',
'clearHistory': 'உள்ளீடுகளை நீக்கு',
'noHistoryFound': 'உள்ளீடுகள் இல்லை',
'serviceTitle': 'பதிவிறக்க சேவை',
'serviceText': 'இயக்கத்தில் உள்ளது',
'network': 'வலைப்பின்னல்',
'proxy': 'பதிலி',
'noProxy': 'பதிலி இல்லாமல் ',
'systemProxy': 'இயல்புநிலை பதிலி',
'customProxy': 'மாற்றியமைக்கப்பட்ட பதிலி',
'server': 'சேவையகம்',
'username': 'பயனர் பெயர்',
'password': 'கடவுச்சொல்',
'thanks': 'நன்றி',
'thanksDesc':
'Gopeed சமூகத்தை உருவாக்கவும் மற்றும் மேம்படுத்த உதவிய அனைத்து பங்களிப்பாளர்களுக்கும் நன்றி!',
'browserExtension': 'உலாவி நீட்டிப்பு',
'launchAtStartup': 'தொடக்கத்தின் போது இயங்கு',
'skipVerifyCert': 'திருத்தி சான்று சரிபார்க்கவும்',
},
};
+26 -12
View File
@@ -17,12 +17,11 @@ const trTR = {
'create': 'Görev oluştur', 'create': 'Görev oluştur',
'directDownload': 'Doğrudan indir', 'directDownload': 'Doğrudan indir',
'advancedOptions': 'Gelişmiş ayarlar', 'advancedOptions': 'Gelişmiş ayarlar',
'followSettings': 'Ayarları takip et',
'downloadLink': 'İndirme bağlantısı', 'downloadLink': 'İndirme bağlantısı',
'downloadLinkValid': 'Lütfen indirme bağlantısını girin', 'downloadLinkValid': 'Lütfen indirme bağlantısını girin',
'downloadLinkHit': 'downloadLinkHit': 'Lütfen indirme bağlantısını girin, HTTP/HTTPS/MAGNET desteklenir@append',
'Lütfen indirme bağlantısını girin, HTTP/HTTPS/MAGNET desteklenir@append', 'downloadLinkHitDesktop': ', veya torrent dosyasını doğrudan buraya sürükleyin',
'downloadLinkHitDesktop':
', veya torrent dosyasını doğrudan buraya sürükleyin',
'download': 'İndir', 'download': 'İndir',
'noFileSelected': 'Devam etmek için lütfen en az bir dosya seçin.', 'noFileSelected': 'Devam etmek için lütfen en az bir dosya seçin.',
'noStoragePermission': 'Depolama izni gerekli', 'noStoragePermission': 'Depolama izni gerekli',
@@ -36,16 +35,15 @@ const trTR = {
'connections': 'Bağlantılar', 'connections': 'Bağlantılar',
'useServerCtime': 'Dosya oluşturma için sunucu saatini kullan', 'useServerCtime': 'Dosya oluşturma için sunucu saatini kullan',
'maxRunning': 'Maksimum çalışan görev sayısı', 'maxRunning': 'Maksimum çalışan görev sayısı',
'defaultDirectDownload': 'Doğrudan indir seçili olsun',
'items': '@count tane', 'items': '@count tane',
'subscribeTracker': 'İzleyiciye abone ol', 'subscribeTracker': 'İzleyiciye abone ol',
'subscribeFail': 'subscribeFail': 'Abone olunamadı, lütfen ağı kontrol edin veya daha sonra tekrar deneyin',
'Abone olunamadı, lütfen ağı kontrol edin veya daha sonra tekrar deneyin',
'update': 'Güncelle', 'update': 'Güncelle',
'updateDaily': 'Günlük olarak güncelle', 'updateDaily': 'Günlük olarak güncelle',
'lastUpdate': 'Son güncellenme tarihi: @time', 'lastUpdate': 'Son güncellenme tarihi: @time',
'addTracker': 'İzleyici ekle', 'addTracker': 'İzleyici ekle',
'addTrackerHit': 'addTrackerHit': 'Lütfen her satıra bir tane olmak üzere izleyici sunucusu bağlantısını girin',
'Lütfen her satıra bir tane olmak üzere izleyici sunucusu bağlantısını girin',
'ui': 'Arayüz', 'ui': 'Arayüz',
'theme': 'Tema', 'theme': 'Tema',
'themeSystem': 'Sistem', 'themeSystem': 'Sistem',
@@ -58,9 +56,13 @@ const trTR = {
'protocol': 'Protokol', 'protocol': 'Protokol',
'port': 'Bağlantı noktası', 'port': 'Bağlantı noktası',
'apiToken': 'API anahtarı', 'apiToken': 'API anahtarı',
'notSet': 'AYARLI', 'notSet': 'AYARLI DEĞİL',
'set': 'AYARLI DEĞİL', 'set': 'AYARLI',
'portInUse': 'Bağlantı noktası [@port] kullanımda, lütfen bağlantı noktasını değiştirin',
'effectAfterRestart': 'Yeniden başlatma sonrası etki edecektir', 'effectAfterRestart': 'Yeniden başlatma sonrası etki edecektir',
'developer': 'Geliştirici',
'logDirectory': 'Kayıt dizini',
'show': 'Göster',
'startAll': 'Hepsini başlat', 'startAll': 'Hepsini başlat',
'pauseAll': 'Hepsini durdur', 'pauseAll': 'Hepsini durdur',
'deleteTask': 'Görevleri sil', 'deleteTask': 'Görevleri sil',
@@ -84,12 +86,24 @@ const trTR = {
'serviceText': 'Çalışıyor', 'serviceText': 'Çalışıyor',
'network': 'Ağ', 'network': 'Ağ',
'proxy': 'Vekil', 'proxy': 'Vekil',
'noProxy': 'Vekil yok',
'systemProxy': 'Sistem vekili',
'customProxy': 'Özel vekil',
'server': 'Sunucu', 'server': 'Sunucu',
'username': 'Kullanıcı adı', 'username': 'Kullanıcı adı',
'password': 'Şifre', 'password': 'Şifre',
'thanks': 'Teşekkürler', 'thanks': 'Teşekkürler',
'thanksDesc': 'thanksDesc': 'Gopeed topluluğunun oluşmasına ve gelişmesine yardımcı olan tüm katılımcılara teşekkürler!',
'Gopeed topluluğunun oluşmasına ve gelişmesine yardımcı olan tüm katılımcılara teşekkürler!',
'browserExtension': 'Tarayıcı uzantısı', 'browserExtension': 'Tarayıcı uzantısı',
'launchAtStartup': 'Başlangıçta başlat',
'seedConfig': 'Seed ayarları',
'seedKeep': 'Manuel durdurulana kadar seed etmeye devam et',
'seedRatio': 'Seed oranı',
'seedTime': 'Seed süresi (dakika)',
'taskDetail': 'Görev detayı',
'taskName': 'Görev adı',
'taskUrl': 'Görev URL',
'downloadPath': 'İndirme yolu',
'skipVerifyCert': 'Sertifika doğrulamasını atla',
}, },
}; };
+1
View File
@@ -89,5 +89,6 @@ const viVN = {
'thanksDesc': 'thanksDesc':
'Cảm ơn tất cả những người đóng góp đã giúp xây dựng và phát triển cộng đồng Gopeed!', 'Cảm ơn tất cả những người đóng góp đã giúp xây dựng và phát triển cộng đồng Gopeed!',
'browserExtension': 'Tiện ích mở rộng trình duyệt', 'browserExtension': 'Tiện ích mở rộng trình duyệt',
'skipVerifyCert': 'Bỏ qua xác thực chứng chỉ',
}, },
}; };
+13
View File
@@ -17,6 +17,7 @@ const zhCN = {
'create': '创建任务', 'create': '创建任务',
'directDownload': '直接下载', 'directDownload': '直接下载',
'advancedOptions': '高级选项', 'advancedOptions': '高级选项',
'followSettings': '跟随设置',
'downloadLink': '下载链接', 'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接', 'downloadLinkValid': '请输入下载链接',
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append', 'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
@@ -34,6 +35,7 @@ const zhCN = {
'connections': '连接数', 'connections': '连接数',
'useServerCtime': '创建文件使用服务器时间', 'useServerCtime': '创建文件使用服务器时间',
'maxRunning': '最大下载数', 'maxRunning': '最大下载数',
'defaultDirectDownload': '默认勾选直接下载',
'items': '@count 项', 'items': '@count 项',
'subscribeTracker': '订阅 Tracker', 'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试', 'subscribeFail': '订阅失败,请检查网络或稍后重试',
@@ -58,6 +60,8 @@ const zhCN = {
'set': '已设置', 'set': '已设置',
'portInUse': '端口[@port]已被占用,请更换端口', 'portInUse': '端口[@port]已被占用,请更换端口',
'effectAfterRestart': '此配置项将在重启应用后生效', 'effectAfterRestart': '此配置项将在重启应用后生效',
'developer': '开发者',
'logDirectory': '日志目录',
'show': '显示', 'show': '显示',
'startAll': '全部开始', 'startAll': '全部开始',
'pauseAll': '全部暂停', 'pauseAll': '全部暂停',
@@ -92,5 +96,14 @@ const zhCN = {
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!', 'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
'browserExtension': '浏览器扩展', 'browserExtension': '浏览器扩展',
'launchAtStartup': '开机自动运行', 'launchAtStartup': '开机自动运行',
'seedConfig': '做种设置',
'seedKeep': '持续做种直到手动停止',
'seedRatio': '做种分享率',
'seedTime': '做种时间(分钟)',
'taskDetail': '任务详情',
'taskName': '任务名称',
'taskUrl': '任务链接',
'downloadPath': '下载路径',
'skipVerifyCert': '跳过证书验证',
} }
}; };
+65 -52
View File
@@ -1,96 +1,109 @@
const zhTW = { const zhTW = {
"zh_TW": { "zh_TW": {
'label': '中文 (正體) ', 'label': '中文 (正體)',
'error': '錯誤', 'error': '錯誤',
'tip': '提示', 'tip': '提示',
'confirm': '確定', 'confirm': '確認',
'cancel': '取消', 'cancel': '取消',
'on': '開啟', 'on': '開啟',
'off': '關閉', 'off': '關閉',
'selectAll': '全選', 'selectAll': '全選',
'task': '任務', 'task': '任務',
'downloading': '下載中', 'downloading': '下載中',
'downloaded': '已完成', 'downloaded': '已下載',
'setting': '設定', 'setting': '設定',
'donate': '鬥內', 'donate': '捐款',
'exit': '離開', 'exit': '退出',
'create': '新增任務', 'create': '建立任務',
'directDownload': '直接下載', 'directDownload': '直接下載',
'advancedOptions': '進階選項', 'advancedOptions': '進階選項',
'followSettings': '跟隨設定',
'downloadLink': '下載連結', 'downloadLink': '下載連結',
'downloadLinkValid': '請輸入下載連結', 'downloadLinkValid': '請輸入下載連結',
'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append', 'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append',
'downloadLinkHitDesktop': ',也可以通過拖拽Torrent檔至此以新增任務', 'downloadLinkHitDesktop': ',或直接拖曳種子檔到此處',
'download': '下載', 'download': '下載',
'noFileSelected': '沒有選取的檔案', 'noFileSelected': '請至少選擇一個檔案以繼續。',
'noStoragePermission': '沒有存儲權', 'noStoragePermission': '需要儲存空間權限',
'selectFile': '選擇檔案', 'selectFile': '選擇檔案',
'rename': '重命名', 'rename': '重新命名',
'basic': '一般', 'basic': '基本',
'advanced': '進階', 'advanced': '進階',
'general': '通用設定', 'general': '一般',
'downloadDir': '下載目錄', 'downloadDir': '下載目錄',
'downloadDirValid': '請選擇下載目錄', 'downloadDirValid': '請選擇下載目錄',
'connections': '同時連接數', 'connections': '連接數',
'useServerCtime': '使用伺服器時間作為檔案建立時間', 'useServerCtime': '使用伺服器時間作為檔案建立時間',
'maxRunning': '最大同時下載數', 'maxRunning': '最大執行任務數',
'items': '@count 項', 'defaultDirectDownload': '預設勾選直接下載',
'subscribeTracker': '已訂閱的追蹤者列表', 'items': '@count 個項目',
'subscribeFail': '訂閱失敗', 'subscribeTracker': '訂閱 Tracker',
'subscribeFail': '訂閱失敗,請檢查網路或稍後再試',
'update': '更新', 'update': '更新',
'updateDaily': '每天自動更新', 'updateDaily': '每日更新',
'lastUpdate': '上次更新時間: @time', 'lastUpdate': '最後更新時間:@time',
'addTracker': '增加新的追蹤者', 'addTracker': '新增 Tracker',
'addTrackerHit': '請輸入追蹤者伺服器地址,每行一條', 'addTrackerHit': '請輸入 Tracker 伺服器 URL,每行一個',
'ui': '介面', 'ui': '介面',
'theme': '主題', 'theme': '主題',
'themeSystem': '跟隨系統', 'themeSystem': '系統',
'themeLight': '亮色主題', 'themeLight': '淺色',
'themeDark': '深色主題', 'themeDark': '深色',
'locale': '語言', 'locale': '語言',
'about': '關於', 'about': '關於',
'homepage': '訪問主頁', 'homepage': '首頁',
'version': '版本資訊', 'version': '版本',
'protocol': '通信協定', 'protocol': '協議',
'port': '埠', 'port': '連接埠',
'apiToken': 'API令牌', 'apiToken': 'API Token',
'notSet': '未設定', 'notSet': '未設定',
'set': '已設定', 'set': '已設定',
'portInUse': '端口[@port]已被占用,請更換端口', 'portInUse': '連接埠 [@port] 已被使用,請更改連接埠',
'effectAfterRestart': '重啟後生效', 'effectAfterRestart': '重新啟動後生效',
'developer': '開發者',
'logDirectory': '日誌目錄',
'show': '顯示', 'show': '顯示',
'startAll': '全部開始', 'startAll': '全部開始',
'pauseAll': '全部暫停', 'pauseAll': '全部暫停',
'deleteTask': '刪除任務', 'deleteTask': '刪除任務',
'deleteTaskTip': '保留已經下載的檔案', 'deleteTaskTip': '保留已下載的檔案',
'delete': '刪除', 'delete': '刪除',
'newVersionTitle': '新版本 @version 已發佈', 'newVersionTitle': '發現新版本 @version',
'newVersionUpdate': '下載', 'newVersionUpdate': '立即更新',
'newVersionLater': '取消', 'newVersionLater': '稍後',
'extensions': '擴充程式', 'extensions': '擴充功能',
'extensionInstallUrl': '安裝連結', 'extensionInstallUrl': '安裝 URL',
'extensionInstallSuccess': '安裝成功', 'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成', 'extensionUpdateSuccess': '更新成功',
'extensionDelete': '解除安裝', 'extensionDelete': '刪除擴充功能',
'extensionAlreadyLatest': '已是最新版本', 'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得擴充程式', 'extensionFind': '尋找擴充功能',
'extensionDevelop': '開發擴充程式', 'extensionDevelop': '開發擴充功能',
'history': '歷史記錄', 'history': '歷史記錄',
'clearHistory': '清空記錄', 'clearHistory': '清除歷史記錄',
'noHistoryFound': '還沒有記錄', 'noHistoryFound': '未找到歷史記錄',
'serviceTitle': '下載服務', 'serviceTitle': '下載服務',
'serviceText': '執行中', 'serviceText': '運行中',
'network': '網路', 'network': '網路',
'proxy': '代理', 'proxy': 'Proxy',
'noProxy': '不使用代理', 'noProxy': '沒有 Proxy',
'systemProxy': '系統代理', 'systemProxy': '系統 Proxy',
'customProxy': '自定義代理', 'customProxy': '自訂 Proxy',
'server': '伺服器', 'server': '伺服器',
'username': '名稱', 'username': '使用者名稱',
'password': '密碼', 'password': '密碼',
'thanks': '感謝', 'thanks': '感謝',
'thanksDesc': '感謝所有為 Gopeed 社區建設添磚加瓦的貢獻者們!', 'thanksDesc': '感謝所有幫助建立和發展 Gopeed 社群的貢獻者!',
'browserExtension': '瀏覽器擴充程式', 'browserExtension': '瀏覽器擴充功能',
'launchAtStartup': '開機自動運行', 'launchAtStartup': '開機自動執行',
'seedConfig': '做種設定',
'seedKeep': '持續做種直到手動停止',
'seedRatio': '做種分享率',
'seedTime': '做種時間(分鐘)',
'taskDetail': '任務詳情',
'taskName': '任務名稱',
'taskUrl': '任務連結',
'downloadPath': '下載路徑',
'skipVerifyCert': '跳過憑證驗證',
} }
}; };
+10
View File
@@ -2,13 +2,18 @@ import 'package:get/get.dart';
import 'langs/en_us.dart'; import 'langs/en_us.dart';
import 'langs/fa_ir.dart'; import 'langs/fa_ir.dart';
import 'langs/fr_fr.dart';
import 'langs/id_id.dart';
import 'langs/it_it.dart';
import 'langs/ja_jp.dart'; import 'langs/ja_jp.dart';
import 'langs/pl_pl.dart'; import 'langs/pl_pl.dart';
import 'langs/ru_ru.dart'; import 'langs/ru_ru.dart';
import 'langs/ta_ta.dart';
import 'langs/tr_tr.dart'; import 'langs/tr_tr.dart';
import 'langs/vi_vn.dart'; import 'langs/vi_vn.dart';
import 'langs/zh_cn.dart'; import 'langs/zh_cn.dart';
import 'langs/zh_tw.dart'; import 'langs/zh_tw.dart';
import 'langs/es_es.dart';
final messages = _Messages(); final messages = _Messages();
@@ -23,7 +28,12 @@ class _Messages extends Translations {
...faIR, ...faIR,
...jaJP, ...jaJP,
...viVN, ...viVN,
...taTA,
...trTR, ...trTR,
...plPL, ...plPL,
...itIT,
...idID,
...frFR,
...esES,
}; };
} }
+2 -19
View File
@@ -1,7 +1,6 @@
import 'package:args/args.dart'; import 'package:args/args.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@@ -10,7 +9,6 @@ import 'app/modules/app/controllers/app_controller.dart';
import 'app/modules/app/views/app_view.dart'; import 'app/modules/app/views/app_view.dart';
import 'core/libgopeed_boot.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 'util/locale_manager.dart'; import 'util/locale_manager.dart';
import 'util/log_util.dart'; import 'util/log_util.dart';
@@ -77,23 +75,8 @@ Future<void> init(Args args) async {
try { try {
await controller.loadStartConfig(); await controller.loadStartConfig();
final startCfg = controller.startConfig.value; final startCfg = controller.startConfig.value;
// When the app is closed by swiping on the mobile, the backend server is actually still running, don't need to start again. controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg);
final isRunning = api.init(startCfg.network, controller.runningAddress(), startCfg.apiToken);
Util.isMobile() && (await FlutterForegroundTask.isRunningService);
if (!isRunning) {
controller.runningPort.value =
await LibgopeedBoot.instance.start(startCfg);
api.init(
startCfg.network, controller.runningAddress(), startCfg.apiToken);
Database.instance.saveLastRunningConfig(StartConfigEntity(
network: startCfg.network,
address: controller.runningAddress(),
apiToken: startCfg.apiToken));
} else {
final lastRunningConfig = Database.instance.getLastRunningConfig()!;
api.init(lastRunningConfig.network, lastRunningConfig.address,
lastRunningConfig.apiToken);
}
} catch (e) { } catch (e) {
logger.e("libgopeed init fail", e); logger.e("libgopeed init fail", e);
} }
+25 -29
View File
@@ -1,5 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:url_launcher/url_launcher_string.dart';
class FileExplorer { class FileExplorer {
static Future<void> openAndSelectFile(String filePath) async { static Future<void> openAndSelectFile(String filePath) async {
if (await FileSystemEntity.isFile(filePath)) { if (await FileSystemEntity.isFile(filePath)) {
@@ -10,17 +12,7 @@ class FileExplorer {
} }
static Future<void> _openDirectory(String directoryPath) async { static Future<void> _openDirectory(String directoryPath) async {
if (Platform.isWindows) { await launchUrlString("file://$directoryPath");
Process.run('explorer.exe', [directoryPath]);
} else if (Platform.isMacOS) {
Process.run('open', [directoryPath]);
} else if (Platform.isLinux) {
if (await _isUbuntuOrDebian()) {
Process.run('xdg-open', [directoryPath]);
} else if (await _isCentOS()) {
Process.run('nautilus', [directoryPath]);
}
}
} }
static Future<void> _openFile(String filePath) async { static Future<void> _openFile(String filePath) async {
@@ -29,29 +21,33 @@ class FileExplorer {
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
Process.run('open', ['-R', filePath]); Process.run('open', ['-R', filePath]);
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
if (await _isUbuntuOrDebian()) { _linuxOpen(filePath);
Process.run('xdg-open', [filePath]); }
} else if (await _isCentOS()) { }
Process.run('nautilus', ['--select', filePath]);
static Future<void> _linuxOpen(String filePath) async {
if (await Process.run('which', ['xdg-open'])
.then((value) => value.exitCode == 0)) {
final result = await Process.run('xdg-open', [filePath]);
if (result.exitCode != 0) {
_openWithFileManager(filePath);
} }
} else {
_openWithFileManager(filePath);
} }
} }
static Future<bool> _isUbuntuOrDebian() async { static Future<void> _openWithFileManager(String filePath) async {
final result = await Process.run('lsb_release', ['-i']); final desktop = Platform.environment['XDG_CURRENT_DESKTOP'];
if (result.exitCode != 0) { if (desktop == null) {
return false; throw Exception('XDG_CURRENT_DESKTOP is not set');
} }
final output = result.stdout.toString().toLowerCase(); if (desktop == 'GNOME') {
return output.contains('ubuntu') || output.contains('debian'); await Process.run('nautilus', ['--select', filePath]);
} } else if (desktop == 'KDE') {
await Process.run('dolphin', ['--select', filePath]);
static Future<bool> _isCentOS() async { } else {
final result = await Process.run('cat', ['/etc/os-release']); throw Exception('Unsupported desktop environment');
if (result.exitCode != 0) {
return false;
} }
final output = result.stdout.toString().toLowerCase();
return output.contains('centos');
} }
} }
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff. Show more