mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b85e545e | ||
|
|
c5af916952 | ||
|
|
a1981328aa | ||
|
|
51244133e0 | ||
|
|
0c17093b1a | ||
|
|
6538b29b62 | ||
|
|
81e90e6b89 | ||
|
|
4f39296664 | ||
|
|
ece5b91dc2 | ||
|
|
d9f04ae928 | ||
|
|
e3ccecbd9c | ||
|
|
fb1c2cb060 | ||
|
|
9bff75464d | ||
|
|
863f759248 | ||
|
|
f89fb5e438 | ||
|
|
7ca1cc8c64 | ||
|
|
1f791c66e6 | ||
|
|
4d60c47d39 | ||
|
|
3fd3dd49d2 | ||
|
|
d119de7b98 | ||
|
|
69b359a9c9 | ||
|
|
4fbe13a1f0 | ||
|
|
3b50465708 | ||
|
|
473c58796c | ||
|
|
fab8f135f3 | ||
|
|
0fca7b6bc7 | ||
|
|
4700d0ba28 | ||
|
|
6e31ea089f | ||
|
|
4a2088f6fb | ||
|
|
17c8fb7e4d | ||
|
|
8c99bbc847 | ||
|
|
06c7fe7a22 | ||
|
|
8b3b5d4394 | ||
|
|
2ee73ab619 | ||
|
|
15413dbc95 | ||
|
|
c7c43fac80 | ||
|
|
4283bdfa5a | ||
|
|
328ba8592c | ||
|
|
a191a4140d | ||
|
|
3521837964 | ||
|
|
b30cd78f86 | ||
|
|
0583e4aaa6 | ||
|
|
a89c55b5c4 | ||
|
|
68c40d7f3f | ||
|
|
42a0b89d95 | ||
|
|
1860f534cd | ||
|
|
5c2d2ad7b4 | ||
|
|
db6352ada8 | ||
|
|
d1a5ed8b3d | ||
|
|
be9583be0f | ||
|
|
d2c70b077b | ||
|
|
53b3eea4fc | ||
|
|
54347f41b6 | ||
|
|
cea454b015 |
No files matched your search
@@ -0,0 +1,16 @@
|
||||
.git
|
||||
|
||||
*.data
|
||||
*.log
|
||||
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
.github
|
||||
_docs
|
||||
_examples
|
||||
bin
|
||||
ui
|
||||
@@ -0,0 +1 @@
|
||||
* @monkeyWie
|
||||
@@ -11,6 +11,9 @@ categories:
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: '🔧 Performance Improvements'
|
||||
labels:
|
||||
- 'perf'
|
||||
- title: '🧪 Tests'
|
||||
label: 'test'
|
||||
- title: '🧰 Maintenance'
|
||||
|
||||
+56
-16
@@ -13,7 +13,7 @@ on:
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.21"
|
||||
GO_VERSION: "1.22"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
|
||||
permissions:
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
overwrite: true
|
||||
build-macos-arm64-lib:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: macos-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -147,16 +147,14 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
go install src.techknowlogick.com/xgo@latest
|
||||
xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
||||
mv libgopeed-*.dylib libgopeed.dylib
|
||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: macos-arm64-lib
|
||||
path: libgopeed.dylib
|
||||
build-macos:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-13
|
||||
needs: [ get-release, build-macos-arm64-lib ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -171,7 +169,7 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Install appdmg
|
||||
run: |
|
||||
python3 -m pip install setuptools
|
||||
python3 -m pip install setuptools --break-system-packages
|
||||
npm install -g appdmg
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -260,9 +258,6 @@ jobs:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Setup LXD
|
||||
uses: canonical/[email protected]
|
||||
with:
|
||||
@@ -300,20 +295,55 @@ jobs:
|
||||
|
||||
apps:
|
||||
gopeed:
|
||||
command: gopeed
|
||||
command: bin/gopeed
|
||||
extensions: [gnome]
|
||||
plugs:
|
||||
- network
|
||||
- home
|
||||
slots:
|
||||
- dbus-gopeed
|
||||
parts:
|
||||
flutter-git:
|
||||
source: https://github.com/flutter/flutter.git
|
||||
source-tag: $FLUTTER_VERSION
|
||||
source-depth: 1
|
||||
plugin: nil
|
||||
override-build: |
|
||||
mkdir -p \$CRAFT_PART_INSTALL/usr/bin
|
||||
mkdir -p \$CRAFT_PART_INSTALL/usr/libexec
|
||||
cp -r \$CRAFT_PART_SRC \$CRAFT_PART_INSTALL/usr/libexec/flutter
|
||||
ln -s \$CRAFT_PART_INSTALL/usr/libexec/flutter/bin/flutter \$CRAFT_PART_INSTALL/usr/bin/flutter
|
||||
ln -s \$SNAPCRAFT_PART_INSTALL/usr/libexec/flutter/bin/dart \$SNAPCRAFT_PART_INSTALL/usr/bin/dart
|
||||
\$CRAFT_PART_INSTALL/usr/bin/flutter doctor
|
||||
build-packages:
|
||||
- clang
|
||||
- cmake
|
||||
- curl
|
||||
- libgtk-3-dev
|
||||
- ninja-build
|
||||
- unzip
|
||||
- xz-utils
|
||||
- zip
|
||||
override-prime: ''
|
||||
zenity:
|
||||
plugin: nil
|
||||
stage-packages:
|
||||
- zenity
|
||||
gopeed:
|
||||
after: [flutter-git,zenity]
|
||||
source: .
|
||||
plugin: flutter
|
||||
plugin: nil
|
||||
stage-packages:
|
||||
- libgtk-3-dev
|
||||
- libappindicator3-dev
|
||||
flutter-target: lib/main.dart
|
||||
override-build: |
|
||||
# work around pub get stack overflow # https://github.com/dart-lang/sdk/issues/51068#issuecomment-1396588253
|
||||
set +e
|
||||
dart pub get
|
||||
set -eux
|
||||
flutter build linux --release -v
|
||||
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
||||
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
||||
EOF
|
||||
|
||||
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
||||
@@ -350,7 +380,7 @@ jobs:
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
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 -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
|
||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||
flutter build apk
|
||||
@@ -381,7 +411,7 @@ jobs:
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
gomobile init
|
||||
gomobile bind -tags nosqlite,netgo -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build ios --no-codesign
|
||||
mkdir Payload
|
||||
@@ -453,6 +483,9 @@ jobs:
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
@@ -463,11 +496,19 @@ jobs:
|
||||
with:
|
||||
username: liwei2633
|
||||
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
|
||||
uses: docker/build-push-action@v2
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
||||
build-args: |
|
||||
VERSION=${{ env.VERSION }}
|
||||
@@ -477,7 +518,6 @@ jobs:
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
tags: |
|
||||
liwei2633/gopeed:latest
|
||||
liwei2633/gopeed:v${{ env.VERSION }}
|
||||
@@ -17,7 +17,7 @@ on:
|
||||
- "Dockerfile"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.19"
|
||||
GO_VERSION: "1.22"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -27,7 +27,7 @@ on:
|
||||
- "go.sum"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.21"
|
||||
GO_VERSION: "1.22"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
|
||||
jobs:
|
||||
@@ -54,9 +54,10 @@ jobs:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Run Unit tests.
|
||||
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:
|
||||
files: ./coverage.txt
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
build:
|
||||
name: Build check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -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.
|
||||
|
||||
Please refer to `en_us.dart` for translation.
|
||||
words prefixed with `@` are not meant to be translated.
|
||||
|
||||
## flutter development
|
||||
|
||||
|
||||
+1
-9
@@ -1,16 +1,8 @@
|
||||
FROM instrumentisto/flutter:3.16.8 AS flutter
|
||||
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
|
||||
FROM golang:1.22.6 AS go
|
||||
WORKDIR /app
|
||||
COPY ./go.mod ./go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
COPY --from=flutter /app/build/web ./cmd/web/dist
|
||||
ARG VERSION=dev
|
||||
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" \
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
@@ -172,7 +174,7 @@ command:
|
||||
- android
|
||||
|
||||
```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
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
@@ -100,7 +102,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
||||
|
||||
### 環境
|
||||
|
||||
1. Go 言語 1.21+
|
||||
1. Go 言語 1.22+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### クローン
|
||||
@@ -160,7 +162,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```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
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
@@ -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
|
||||
|
||||
1. Golang 1.21+
|
||||
1. Golang 1.22+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### Clone
|
||||
@@ -168,7 +170,7 @@ command:
|
||||
- android
|
||||
|
||||
```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
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@
|
||||
[](https://docs.gopeed.com/zh/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
@@ -112,7 +114,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
|
||||
|
||||
### 环境要求
|
||||
|
||||
1. Golang 1.21+
|
||||
1. Golang 1.22+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### 克隆项目
|
||||
@@ -172,7 +174,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```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
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
@@ -108,7 +110,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
|
||||
### 開發環境
|
||||
|
||||
1. Golang 1.21+
|
||||
1. Golang 1.22+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### 克隆項目
|
||||
@@ -168,7 +170,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```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
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+36
-11
@@ -8,7 +8,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
//go:embed banner.txt
|
||||
@@ -25,23 +27,46 @@ func Start(cfg *model.StartConfig) {
|
||||
panic(err)
|
||||
}
|
||||
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
|
||||
var downloadDir string
|
||||
if base.InDocker == "true" {
|
||||
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||
} else {
|
||||
userDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
downloadDir = filepath.Join(userDir, "Downloads")
|
||||
if downloadDir == "" {
|
||||
if base.InDocker == "true" {
|
||||
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||
} else {
|
||||
userDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
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())
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func watchExit() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
fmt.Printf("Server is shutting down due to signal: %s\n", sig)
|
||||
rest.Downloader.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
+6
-2
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@@ -15,6 +16,8 @@ type args struct {
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
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
|
||||
}
|
||||
@@ -25,7 +28,7 @@ func parse() *args {
|
||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||
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.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
flag.Parse()
|
||||
@@ -57,8 +60,9 @@ func loadConfig(path string) *args {
|
||||
var args args
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
path = filepath.Join(dir, path)
|
||||
|
||||
@@ -48,6 +48,7 @@ func main() {
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: filepath.Join(dir, "storage"),
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
chown -R ${PUID}:${PGID} /app
|
||||
|
||||
|
||||
@@ -1,54 +1,58 @@
|
||||
module github.com/GopeedLab/gopeed
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
||||
github.com/anacrolix/torrent v1.53.3
|
||||
github.com/anacrolix/torrent v1.56.1
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
||||
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe
|
||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||
github.com/go-git/go-git/v5 v5.8.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/mattn/go-ieproxy v0.0.12
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||
golang.org/x/sync v0.6.0
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // 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/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||
github.com/anacrolix/chansync v0.4.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
||||
github.com/anacrolix/chansync v0.5.1 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.21.1 // indirect
|
||||
github.com/anacrolix/envpprof v1.3.0 // indirect
|
||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
|
||||
github.com/anacrolix/generics v0.0.2 // 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.15.2 // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
||||
github.com/anacrolix/stm v0.5.0 // indirect
|
||||
github.com/anacrolix/sync v0.5.1 // 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/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // 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/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/edsrzf/mmap-go v1.1.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
@@ -56,62 +60,69 @@ require (
|
||||
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-llsqlite/adapter v0.1.0 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.5.3 // indirect
|
||||
github.com/go-logr/logr v1.4.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/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // 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/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||
github.com/pion/ice/v2 v2.3.12 // indirect
|
||||
github.com/pion/interceptor v0.1.25 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pion/datachannel v1.5.8 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/ice/v2 v2.3.31 // indirect
|
||||
github.com/pion/interceptor v0.1.29 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.13 // indirect
|
||||
github.com/pion/rtp v1.8.3 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.7 // indirect
|
||||
github.com/pion/sctp v1.8.19 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.20 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
||||
github.com/pion/transport/v2 v2.2.9 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/webrtc/v3 v3.2.50 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // 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/skeema/knownhosts v1.2.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tidwall/btree v1.7.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.3 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.40.6 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
modernc.org/libc v1.55.6 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
zombiezen.com/go/sqlite v1.1.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.31.1 // indirect
|
||||
zombiezen.com/go/sqlite v1.3.0 // indirect
|
||||
)
|
||||
@@ -5,17 +5,20 @@ crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/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/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/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
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.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
|
||||
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||
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/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
||||
@@ -23,33 +26,35 @@ github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ
|
||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
|
||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
|
||||
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
|
||||
github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
|
||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
|
||||
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-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-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anacrolix/chansync v0.4.0 h1:Md0HM7zYCAO4KwNwgcIRgxNsMxiRuk7D1Ha0Uo+2y60=
|
||||
github.com/anacrolix/chansync v0.4.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
||||
github.com/anacrolix/dht/v2 v2.21.0 h1:8nzI+faaynY9jOKmVgdmBZVrTo8B7ZE/LKEgN3Vl/Bs=
|
||||
github.com/anacrolix/dht/v2 v2.21.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
|
||||
github.com/anacrolix/chansync v0.5.1 h1:j+R9DtotkXm40VFjZ8rJTSJkg2Gv1ldZt8kl96lyJJ0=
|
||||
github.com/anacrolix/chansync v0.5.1/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
||||
github.com/anacrolix/dht/v2 v2.21.1 h1:s1rKkfLLcmBHKv4v/mtMkIeHIEptzEFiB6xVu54+5/o=
|
||||
github.com/anacrolix/dht/v2 v2.21.1/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
|
||||
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.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.3.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-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so=
|
||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||
github.com/anacrolix/generics v0.0.2 h1:UbtD+KntUGxeGYMC4RwhsETieL9ixGdSptJQRhdy7No=
|
||||
github.com/anacrolix/generics v0.0.2/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||
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/log v0.3.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.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.14.6-0.20231202035202-ed7a02cad0b4/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
||||
github.com/anacrolix/log v0.15.2 h1:LTSf5Wm6Q4GNWPFMBP7NPYV6UBVZzZLKckL+/Lj72Oo=
|
||||
github.com/anacrolix/log v0.15.2/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/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
|
||||
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
|
||||
@@ -79,13 +84,14 @@ github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC
|
||||
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.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||
github.com/anacrolix/torrent v1.53.3 h1:Abb3J8p/JWUjOzHaii8kixuDOGMXz+Oh8+hwc/zA2lA=
|
||||
github.com/anacrolix/torrent v1.53.3/go.mod h1:is8GNob5qDeZ5Kq+pKPiE2xqYUi1ms7IgSB+CftZETk=
|
||||
github.com/anacrolix/upnp v0.1.3 h1:NlYEhE75adz2npEJKjbqyqnyW9qU4STookvSNXBJ5ao=
|
||||
github.com/anacrolix/upnp v0.1.3/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
||||
github.com/anacrolix/torrent v1.56.1 h1:QeJMOP0NuhpQ5dATsOqEL0vUO85aPMNMGP2FACNt0Eg=
|
||||
github.com/anacrolix/torrent v1.56.1/go.mod h1:5DMHbeIM1TuC5wTQ99XieKKLiYZYz6iB2lyZpKZEr6w=
|
||||
github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
|
||||
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
||||
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
||||
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/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
@@ -105,10 +111,9 @@ github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2w
|
||||
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/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/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/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
@@ -117,18 +122,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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.7.0/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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
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/dop251/goja v0.0.0-20240806095544-3491d4a58fbe h1:jwFJkgsdelB87ohlXaAGSd05Cb5ALDFa9iW9IGRHcRM=
|
||||
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe/go.mod h1:DF+w/nLMIkvRpyhd/0K+Okbh3fVZBtXLwRtS/ccAa5w=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z9xEeRmDX2L4ocE0ETKcHKw6MVL3R+co=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
|
||||
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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -139,6 +139,7 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
|
||||
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
|
||||
github.com/edsrzf/mmap-go v1.1.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/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
@@ -146,9 +147,10 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
|
||||
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/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.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/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-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
@@ -160,25 +162,25 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS
|
||||
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
|
||||
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
|
||||
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
|
||||
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
|
||||
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-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/crawshaw v0.5.0 h1:Olbqkth53vkkh4WvmkYjrtfOBcxXD3rMYBYuk6FNH3E=
|
||||
github.com/go-llsqlite/crawshaw v0.5.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
|
||||
github.com/go-llsqlite/crawshaw v0.5.3 h1:PmHXJjjxHodMoFWs3dEcJ1I1m9a+J0TCMNiHjGbXy1o=
|
||||
github.com/go-llsqlite/crawshaw v0.5.3/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
|
||||
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-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.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
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-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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -196,9 +198,6 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
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.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -209,16 +208,15 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
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.4.0/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/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-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
@@ -229,18 +227,18 @@ 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.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
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.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/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/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
github.com/huandu/xstrings v1.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.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
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/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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -251,6 +249,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -270,82 +270,82 @@ github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
||||
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/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.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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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-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 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.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
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/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/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.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.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/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/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
|
||||
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts=
|
||||
github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s=
|
||||
github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
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/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.31 h1:qag/YqiOn5qPi0kgeVdsytxjx8szuriWSIeXKu8dDQc=
|
||||
github.com/pion/ice/v2 v2.3.31/go.mod h1:8fac0+qftclGy1tYd/nfwfHC729BLaxtVqMdMVCAVPU=
|
||||
github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M=
|
||||
github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4=
|
||||
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/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
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/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/rtp v1.8.7 h1:qslKkG8qxvQ7hqaxkmL7Pl0XcUm+/Er7nMnu6Vq+ZxM=
|
||||
github.com/pion/rtp v1.8.7/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.19 h1:2CYuw+SQ5vkQ9t0HdOPccsCz1GQMDuVy5PglLgKVBW8=
|
||||
github.com/pion/sctp v1.8.19/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||
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/v2 v2.2.8/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v2 v2.2.9 h1:WEDygVovkJlV2CCunM9KS2kds+kcl7zdIefQA5y/nkE=
|
||||
github.com/pion/transport/v2 v2.2.9/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4=
|
||||
github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0=
|
||||
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/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.50 h1:C/rwL2mBfCxHv6tlLzDAO3krJpQXfVx8A8WHnGJ2j34=
|
||||
github.com/pion/webrtc/v3 v3.2.50/go.mod h1:dytYYoSBy7ZUWhJMbndx9UckgYvzNAfL7xgVnrIKxqo=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -385,7 +385,6 @@ 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/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/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/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
@@ -397,23 +396,27 @@ 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/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/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.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.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.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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.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.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.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.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.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/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
@@ -423,54 +426,53 @@ github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 h1:x9pIfbdIjn
|
||||
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.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
|
||||
github.com/wlynxg/anet v0.0.3/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/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/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
||||
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.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
||||
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.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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.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.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
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/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
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-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -482,26 +484,20 @@ 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-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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
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-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-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.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
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.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
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/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -510,12 +506,11 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20201020160332-67f06af15bc9/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -525,27 +520,19 @@ 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-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-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-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-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-20200413165638-669c56c373c4/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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -553,40 +540,35 @@ 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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.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-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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
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/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
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.3/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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
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.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.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
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=
|
||||
@@ -596,12 +578,11 @@ 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-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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -620,9 +601,6 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
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.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/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=
|
||||
@@ -638,7 +616,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -646,13 +623,31 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/libc v1.40.6 h1:141JHq3SjhOOCjECBgD4K8VgTFOy19CnHwroC08DAig=
|
||||
modernc.org/libc v1.40.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
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/v4 v4.20.5 h1:s04akhT2dysD0DFOlv9fkQ6oUTLPYgMnnDk9oaqjszM=
|
||||
modernc.org/ccgo/v4 v4.20.5/go.mod h1:fYXClPUMWxWaz1Xj5sHbzW/ZENEFeuHLToqBxUk41nE=
|
||||
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.4.3 h1:Ik4ZcMbC7aY4ZDPUhzXVXi7GMub9QcXLTfXn3mWpNw8=
|
||||
modernc.org/gc/v2 v2.4.3/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/libc v1.55.6 h1:WfOvqnRFSjVT3WZIAVzGkKcYSk/toAXXnPAMWnTKBZc=
|
||||
modernc.org/libc v1.55.6/go.mod h1:JXguUpMkbw1gknxspNE9XaG+kk9hDAAnBxpA6KGLiyA=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
zombiezen.com/go/sqlite v1.1.0 h1:2BIABTFCE221++xR90rV0mE6T5gFF8o0MUcKDNUrIVc=
|
||||
zombiezen.com/go/sqlite v1.1.0/go.mod h1:rZEFEJSoeXeiwuAb6HugOJLhtDabl+EekT92DCEKOb0=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
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.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=
|
||||
modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
|
||||
zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY=
|
||||
@@ -1,14 +1,16 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
GetConfig func(v any) bool
|
||||
ProxyUrl *url.URL
|
||||
GetConfig func(v any)
|
||||
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
||||
FileController
|
||||
//ContextDialer() (proxy.Dialer, error)
|
||||
}
|
||||
@@ -22,10 +24,9 @@ type DefaultFileController struct {
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
GetConfig: func(v any) {},
|
||||
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
||||
FileController: &DefaultFileController{},
|
||||
GetConfig: func(v any) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
// Name return the name of the protocol.
|
||||
Name() string
|
||||
|
||||
Setup(ctl *controller.Controller)
|
||||
// Resolve resource info from request
|
||||
Resolve(req *base.Request) error
|
||||
@@ -31,6 +29,12 @@ type Fetcher interface {
|
||||
Wait() error
|
||||
}
|
||||
|
||||
type Uploader interface {
|
||||
Upload() error
|
||||
UploadedBytes() int64
|
||||
WaitUpload() error
|
||||
}
|
||||
|
||||
// FetcherMeta defines the meta information of a fetcher.
|
||||
type FetcherMeta struct {
|
||||
Req *base.Request `json:"req"`
|
||||
@@ -68,17 +72,57 @@ func (m *FetcherMeta) RootDirPath() string {
|
||||
}
|
||||
}
|
||||
|
||||
// FetcherBuilder defines the interface for a fetcher builder.
|
||||
type FetcherBuilder interface {
|
||||
// Schemes returns the schemes supported by the fetcher.
|
||||
Schemes() []string
|
||||
type FilterType int
|
||||
|
||||
const (
|
||||
// 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() 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 Fetcher) (any, error)
|
||||
// Restore fetcher
|
||||
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
||||
// Close the fetcher manager, release resources.
|
||||
Close() error
|
||||
}
|
||||
|
||||
type DefaultFetcher struct {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package fetcher
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSchemeFilter_Match(t *testing.T) {
|
||||
type fields struct {
|
||||
Type FilterType
|
||||
Pattern string
|
||||
}
|
||||
type args struct {
|
||||
uri string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "url match",
|
||||
fields: fields{
|
||||
Type: FilterTypeUrl,
|
||||
Pattern: "https",
|
||||
},
|
||||
args: args{
|
||||
uri: "https://github.com",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "url not match",
|
||||
fields: fields{
|
||||
Type: FilterTypeUrl,
|
||||
Pattern: "https",
|
||||
},
|
||||
args: args{
|
||||
uri: "ftp://github.com",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "file match",
|
||||
fields: fields{
|
||||
Type: FilterTypeFile,
|
||||
Pattern: "torrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "d:/temp/test.torrent",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "file not match",
|
||||
fields: fields{
|
||||
Type: FilterTypeFile,
|
||||
Pattern: "torrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "d:/temp/test.txt",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "base64 match",
|
||||
fields: fields{
|
||||
Type: FilterTypeBase64,
|
||||
Pattern: "application/x-bittorrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "data:application/x-bittorrent;base64,xxx",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "base64 not match",
|
||||
fields: fields{
|
||||
Type: FilterTypeBase64,
|
||||
Pattern: "application/x-bittorrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "data:application/javascript;base64,xxx",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SchemeFilter{
|
||||
Type: tt.fields.Type,
|
||||
Pattern: tt.fields.Pattern,
|
||||
}
|
||||
if got := s.Match(tt.args.uri); got != tt.want {
|
||||
t.Errorf("Match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,10 @@ package bt
|
||||
type config struct {
|
||||
ListenPort int `json:"listenPort"`
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+284
-90
@@ -2,6 +2,7 @@ package bt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -23,6 +24,8 @@ var (
|
||||
lock sync.Mutex
|
||||
torrentDirMap = make(map[string]string)
|
||||
ftMap = make(map[string]*fileTorrentImpl)
|
||||
closeCtx context.Context
|
||||
closeFunc func()
|
||||
)
|
||||
|
||||
type Fetcher struct {
|
||||
@@ -31,15 +34,13 @@ type Fetcher struct {
|
||||
|
||||
torrent *torrent.Torrent
|
||||
meta *fetcher.FetcherMeta
|
||||
data *fetcherData
|
||||
|
||||
torrentReady atomic.Bool
|
||||
torrentDrop atomic.Bool
|
||||
create atomic.Bool
|
||||
progress fetcher.Progress
|
||||
}
|
||||
|
||||
func (f *Fetcher) Name() string {
|
||||
return "bt"
|
||||
torrentReady atomic.Bool
|
||||
torrentUpload atomic.Bool
|
||||
torrentDropCtx context.Context
|
||||
torrentDropFunc func()
|
||||
uploadDoneCh chan any
|
||||
}
|
||||
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
@@ -47,13 +48,12 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
exist := f.ctl.GetConfig(&f.config)
|
||||
if !exist {
|
||||
f.config = &config{
|
||||
ListenPort: 0,
|
||||
Trackers: []string{},
|
||||
}
|
||||
if f.data == nil {
|
||||
f.data = &fetcherData{}
|
||||
}
|
||||
f.uploadDoneCh = make(chan any, 1)
|
||||
f.torrentDropCtx, f.torrentDropFunc = context.WithCancel(context.Background())
|
||||
f.ctl.GetConfig(&f.config)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -64,14 +64,16 @@ func (f *Fetcher) initClient() (err error) {
|
||||
if client != nil {
|
||||
return
|
||||
}
|
||||
if closeCtx == nil {
|
||||
closeCtx, closeFunc = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
cfg.Seed = true
|
||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||
cfg.ListenPort = f.config.ListenPort
|
||||
if f.ctl.ProxyUrl != nil {
|
||||
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||
}
|
||||
cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
|
||||
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||
ClientBaseDir: cfg.DataDir,
|
||||
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||
@@ -81,7 +83,17 @@ func (f *Fetcher) initClient() (err error) {
|
||||
ftMap[infoHash.String()] = ft
|
||||
},
|
||||
})
|
||||
dnsResolver := &DnsCacheResolver{RefreshTimeout: 5 * time.Minute}
|
||||
cfg.TrackerDialContext = dnsResolver.DialContext
|
||||
client, err = torrent.NewClient(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
closeCtx, closeFunc = context.WithCancel(context.Background())
|
||||
go func() {
|
||||
dnsResolver.Run(closeCtx)
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,42 +101,42 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f.addTorrent(req); err != nil {
|
||||
f.meta.Req = req
|
||||
if err := f.addTorrent(req, false); err != nil {
|
||||
return err
|
||||
}
|
||||
f.updateRes()
|
||||
f.meta.Req = req
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||
f.create.Store(true)
|
||||
f.meta.Opts = opts
|
||||
if f.meta.Res != nil {
|
||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
||||
torrentDirMap[f.meta.Res.Hash] = opts.Path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Start() (err error) {
|
||||
if !f.torrentReady.Load() {
|
||||
if err = f.addTorrent(f.meta.Req); err != nil {
|
||||
if err = f.addTorrent(f.meta.Req, false); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||
ft.setTorrentDir(f.meta.FolderPath())
|
||||
ft.setTorrentDir(f.meta.Opts.Path)
|
||||
}
|
||||
files := f.torrent.Files()
|
||||
// If the user does not specify the file to download, all files will be downloaded by default
|
||||
if len(f.meta.Opts.SelectFiles) == 0 {
|
||||
f.meta.Opts.SelectFiles = make([]int, len(files))
|
||||
for i := range files {
|
||||
f.meta.Opts.SelectFiles[i] = i
|
||||
if f.data.Progress == nil {
|
||||
if len(f.meta.Opts.SelectFiles) == 0 {
|
||||
f.meta.Opts.SelectFiles = make([]int, len(files))
|
||||
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) {
|
||||
f.torrent.DownloadAll()
|
||||
} else {
|
||||
@@ -133,7 +145,6 @@ func (f *Fetcher) Start() (err error) {
|
||||
file.Download()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,8 +155,12 @@ func (f *Fetcher) Pause() (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) Close() (err error) {
|
||||
f.torrentDrop.Store(false)
|
||||
f.safeDrop()
|
||||
f.torrentDropFunc()
|
||||
f.uploadDoneCh <- nil
|
||||
if len(client.Torrents()) == 0 {
|
||||
err = closeClient()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -158,80 +173,94 @@ func (f *Fetcher) safeDrop() {
|
||||
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 {
|
||||
return f.meta
|
||||
}
|
||||
|
||||
func (f *Fetcher) Stats() any {
|
||||
stats := f.torrent.Stats()
|
||||
baseStats := &bt.Stats{
|
||||
var stats torrent.TorrentStats
|
||||
if f.torrent != nil {
|
||||
stats = f.torrent.Stats()
|
||||
} else {
|
||||
stats = torrent.TorrentStats{}
|
||||
}
|
||||
return &bt.Stats{
|
||||
TotalPeers: stats.TotalPeers,
|
||||
ActivePeers: stats.ActivePeers,
|
||||
ConnectedSeeders: stats.ConnectedSeeders,
|
||||
SeedBytes: f.data.SeedBytes,
|
||||
SeedRatio: f.seedRadio(),
|
||||
SeedTime: f.data.SeedTime,
|
||||
}
|
||||
return baseStats
|
||||
}
|
||||
|
||||
func (f *Fetcher) Progress() fetcher.Progress {
|
||||
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]
|
||||
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() {
|
||||
res := &base.Resource{
|
||||
Name: f.torrent.Name(),
|
||||
Range: true,
|
||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||
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() {
|
||||
res.Files[i] = &base.FileInfo{
|
||||
Name: filepath.Base(file.DisplayPath()),
|
||||
Path: util.Dir(file.Path()),
|
||||
Path: util.Dir(file.DisplayPath()),
|
||||
Size: file.Length(),
|
||||
}
|
||||
}
|
||||
@@ -242,14 +271,84 @@ func (f *Fetcher) updateRes() {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||
func (f *Fetcher) Upload() (err error) {
|
||||
return f.addTorrent(f.meta.Req, true)
|
||||
}
|
||||
|
||||
func (f *Fetcher) doUpload(fromUpload bool) {
|
||||
if !f.torrentUpload.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check and update seed data
|
||||
lastData := &fetcherData{
|
||||
SeedBytes: f.data.SeedBytes,
|
||||
SeedTime: f.data.SeedTime,
|
||||
}
|
||||
var doneTime int64 = 0
|
||||
for {
|
||||
select {
|
||||
case <-f.torrentDropCtx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
if !f.torrentReady.Load() {
|
||||
continue
|
||||
}
|
||||
|
||||
stats := f.torrent.Stats()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
schema := util.ParseSchema(req.URL)
|
||||
if schema == "MAGNET" {
|
||||
f.torrent, err = client.AddMagnet(req.URL)
|
||||
} else if schema == "APPLICATION/X-BITTORRENT" {
|
||||
} else if schema == "DATA" {
|
||||
_, data := util.ParseDataUri(req.URL)
|
||||
buf := bytes.NewBuffer(data)
|
||||
var metaInfo *metainfo.MetaInfo
|
||||
@@ -289,34 +388,129 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||
}
|
||||
<-f.torrent.GotInfo()
|
||||
f.torrentReady.Store(true)
|
||||
|
||||
go f.doUpload(fromUpload)
|
||||
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"}
|
||||
|
||||
func (fb *FetcherBuilder) Schemes() []string {
|
||||
return schemes
|
||||
type fetcherData struct {
|
||||
Progress fetcher.Progress
|
||||
SeedBytes int64
|
||||
// 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{}
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
return nil, nil
|
||||
func (fm *FetcherManager) ParseName(u string) string {
|
||||
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) {
|
||||
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||
func (fm *FetcherManager) AutoRename() bool {
|
||||
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{
|
||||
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
|
||||
func parseBep20() string {
|
||||
s := strings.ReplaceAll(base.Version, ".", "")
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -35,7 +35,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
||||
}
|
||||
|
||||
want := &base.Resource{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
@@ -60,61 +59,162 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||
defer proxyListener.Close()
|
||||
|
||||
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
|
||||
doResolve(t, buildConfigFetcher(&base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
System: false,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
}))
|
||||
}
|
||||
|
||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||
Extra: bt.ReqExtra{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
t.Run("Resolve Single File", func(t *testing.T) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||
Extra: bt.ReqExtra{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
want := &base.Resource{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
want := &base.Resource{
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
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) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
tests := []struct {
|
||||
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 {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher.Setup(controller.NewController())
|
||||
fb := new(FetcherManager)
|
||||
fetcher := fb.Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
}
|
||||
|
||||
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
|
||||
fetcher := new(FetcherManager).Build()
|
||||
newController := controller.NewController()
|
||||
mockCfg := config{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
}}
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
||||
}
|
||||
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
return proxyConfig.ToHandler()
|
||||
}
|
||||
newController.ProxyUrl = proxyUrl
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -5,12 +5,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"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"
|
||||
"mime"
|
||||
"net/http"
|
||||
@@ -21,6 +15,13 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"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 {
|
||||
@@ -64,23 +65,13 @@ type Fetcher struct {
|
||||
eg *errgroup.Group
|
||||
}
|
||||
|
||||
func (f *Fetcher) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
f.ctl = ctl
|
||||
f.doneCh = make(chan error, 1)
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
exist := 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,
|
||||
}
|
||||
}
|
||||
f.ctl.GetConfig(&f.config)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,6 +79,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||
return err
|
||||
}
|
||||
f.meta.Req = req
|
||||
httpReq, err := f.buildRequest(nil, req)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -108,7 +100,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)) {
|
||||
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
|
||||
// response 206 status code, support breakpoint continuation
|
||||
res.Range = true
|
||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||
@@ -120,7 +112,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
res.Size = parse
|
||||
}
|
||||
} 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)
|
||||
if contentLength != "" {
|
||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||
@@ -161,7 +154,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
file.Name = httpReq.URL.Hostname()
|
||||
}
|
||||
res.Files = append(res.Files, file)
|
||||
f.meta.Req = req
|
||||
f.meta.Res = res
|
||||
return nil
|
||||
}
|
||||
@@ -179,8 +171,12 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
||||
opts.Extra = &fhttp.OptsExtra{}
|
||||
}
|
||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||
if extra.Connections == 0 {
|
||||
if extra.Connections <= 0 {
|
||||
extra.Connections = f.config.Connections
|
||||
// Avoid zero connections configuration
|
||||
if extra.Connections <= 0 {
|
||||
extra.Connections = 1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -386,7 +382,8 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
method string
|
||||
body io.Reader
|
||||
)
|
||||
headers := make(map[string][]string)
|
||||
|
||||
headers := http.Header{}
|
||||
if req.Extra == nil {
|
||||
method = http.MethodGet
|
||||
} else {
|
||||
@@ -398,7 +395,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
}
|
||||
if len(extra.Header) > 0 {
|
||||
for k, v := range extra.Header {
|
||||
headers[k] = []string{v}
|
||||
headers.Set(k, v)
|
||||
}
|
||||
}
|
||||
if extra.Body != "" {
|
||||
@@ -406,8 +403,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
}
|
||||
}
|
||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||
// load user agent from config
|
||||
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
|
||||
headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
@@ -451,12 +447,11 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) buildClient() *http.Client {
|
||||
transport := &http.Transport{}
|
||||
transport := &http.Transport{
|
||||
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||
}
|
||||
// Cookie handle
|
||||
jar, _ := cookiejar.New(nil)
|
||||
if f.ctl.ProxyUrl != nil {
|
||||
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Jar: jar,
|
||||
@@ -467,30 +462,67 @@ type fetcherData struct {
|
||||
Chunks []*chunk
|
||||
}
|
||||
|
||||
type FetcherBuilder struct {
|
||||
type FetcherManager struct {
|
||||
}
|
||||
|
||||
var schemes = []string{"HTTP", "HTTPS"}
|
||||
|
||||
func (fb *FetcherBuilder) Schemes() []string {
|
||||
return schemes
|
||||
func (fm *FetcherManager) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
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)
|
||||
return &fetcherData{
|
||||
Chunks: _f.chunks,
|
||||
}, 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 {
|
||||
fd := v.(*fetcherData)
|
||||
fb := &FetcherBuilder{}
|
||||
fb := &FetcherManager{}
|
||||
fetcher := fb.Build().(*Fetcher)
|
||||
fetcher.meta = meta
|
||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||
@@ -501,3 +533,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
||||
return fetcher
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"net"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -193,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 {
|
||||
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||
}
|
||||
@@ -307,7 +356,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fb := new(FetcherBuilder)
|
||||
fb := new(FetcherManager)
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
data, err := fb.Store(fetcher)
|
||||
if err != nil {
|
||||
@@ -338,7 +387,13 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||
fetcher := downloadReady(httpListener, 4, t)
|
||||
ctl := controller.NewController()
|
||||
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
|
||||
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
return (&base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
}).ToHandler()
|
||||
}
|
||||
fetcher.Setup(ctl)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
@@ -356,19 +411,21 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
||||
}
|
||||
|
||||
func buildFetcher() *Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher.Setup(controller.NewController())
|
||||
fm := new(FetcherManager)
|
||||
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)
|
||||
}
|
||||
|
||||
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher := new(FetcherManager).Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(cfg)), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
|
||||
@@ -2,12 +2,12 @@ package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/armon/go-socks5"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -24,6 +24,11 @@ const (
|
||||
Dir = "./"
|
||||
BuildFile = Dir + BuildName
|
||||
|
||||
ExternalDownloadUrl = "https://raw.githubusercontent.com/GopeedLab/gopeed/v1.5.6/_docs/img/banner.png"
|
||||
ExternalDownloadName = "banner.png"
|
||||
ExternalDownloadSize = 26416
|
||||
//ExternalDownloadMd5 = "c67c6e3cae79a95342485676571e8a5c"
|
||||
|
||||
DownloadName = "download.data"
|
||||
DownloadRename = "download (1).data"
|
||||
DownloadFile = Dir + DownloadName
|
||||
|
||||
+147
-6
@@ -3,7 +3,10 @@ package base
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/mattn/go-ieproxy"
|
||||
"golang.org/x/exp/slices"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,6 +16,8 @@ type Request struct {
|
||||
Extra any `json:"extra"`
|
||||
// Labels is used to mark the download task
|
||||
Labels map[string]string `json:"labels"`
|
||||
// Proxy is special proxy config for request
|
||||
Proxy *RequestProxy `json:"proxy"`
|
||||
}
|
||||
|
||||
func (r *Request) Validate() error {
|
||||
@@ -22,6 +27,37 @@ func (r *Request) Validate() error {
|
||||
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
|
||||
type Resource struct {
|
||||
// if name is not empty, the resource is a folder and the name is the folder name
|
||||
@@ -91,12 +127,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
||||
}
|
||||
|
||||
func (o *Options) Clone() *Options {
|
||||
return &Options{
|
||||
Name: o.Name,
|
||||
Path: o.Path,
|
||||
SelectFiles: o.SelectFiles,
|
||||
Extra: o.Extra,
|
||||
}
|
||||
return util.DeepClone(o)
|
||||
}
|
||||
|
||||
func ParseReqExtra[E any](req *Request) error {
|
||||
@@ -128,3 +159,113 @@ func ParseOptsExtra[E any](opts *Options) error {
|
||||
opts.Extra = &t
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||
type DownloaderStoreConfig struct {
|
||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||
|
||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||
Extra map[string]any `json:"extra"`
|
||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.MaxRunning == 0 {
|
||||
cfg.MaxRunning = 5
|
||||
}
|
||||
if cfg.ProtocolConfig == nil {
|
||||
cfg.ProtocolConfig = make(map[string]any)
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *DownloaderStoreConfig {
|
||||
if beforeCfg == nil {
|
||||
return cfg
|
||||
}
|
||||
if cfg.DownloadDir == "" {
|
||||
cfg.DownloadDir = beforeCfg.DownloadDir
|
||||
}
|
||||
if cfg.MaxRunning == 0 {
|
||||
cfg.MaxRunning = beforeCfg.MaxRunning
|
||||
}
|
||||
if cfg.ProtocolConfig == nil {
|
||||
cfg.ProtocolConfig = beforeCfg.ProtocolConfig
|
||||
}
|
||||
if cfg.Extra == nil {
|
||||
cfg.Extra = beforeCfg.Extra
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = beforeCfg.Proxy
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
// System is the flag that use system proxy
|
||||
System bool `json:"system"`
|
||||
Scheme string `json:"scheme"`
|
||||
Host string `json:"host"`
|
||||
Usr string `json:"usr"`
|
||||
Pwd string `json:"pwd"`
|
||||
}
|
||||
|
||||
func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||
if cfg == nil || cfg.Enable == false {
|
||||
return nil
|
||||
}
|
||||
if cfg.System {
|
||||
ieproxy.ReloadConf()
|
||||
return ieproxy.GetProxyFunc()
|
||||
}
|
||||
if cfg.Scheme == "" || cfg.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return http.ProxyURL(util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd))
|
||||
}
|
||||
|
||||
// ToUrl returns the proxy url, just for git clone
|
||||
func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
||||
if cfg == nil || cfg.Enable == false {
|
||||
return nil
|
||||
}
|
||||
if cfg.System {
|
||||
ieproxy.ReloadConf()
|
||||
static := ieproxy.GetConf().Static
|
||||
if static.Active && len(static.Protocols) > 0 {
|
||||
// If only one protocol, use it
|
||||
if len(static.Protocols) == 1 {
|
||||
for _, v := range static.Protocols {
|
||||
return parseUrlSafe(v)
|
||||
}
|
||||
}
|
||||
// Check https
|
||||
if v, ok := static.Protocols["https"]; ok {
|
||||
return parseUrlSafe(v)
|
||||
}
|
||||
// Check http
|
||||
if v, ok := static.Protocols["http"]; ok {
|
||||
return parseUrlSafe(v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if cfg.Scheme == "" || cfg.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
|
||||
}
|
||||
|
||||
func parseUrlSafe(rawUrl string) *url.URL {
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fields *DownloaderStoreConfig
|
||||
want *DownloaderStoreConfig
|
||||
}{
|
||||
{
|
||||
"Init",
|
||||
&DownloaderStoreConfig{},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init MaxRunning",
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init ProtocolConfig",
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init Proxy",
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &DownloaderStoreConfig{
|
||||
FirstLoad: tt.fields.FirstLoad,
|
||||
DownloadDir: tt.fields.DownloadDir,
|
||||
MaxRunning: tt.fields.MaxRunning,
|
||||
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||
Extra: tt.fields.Extra,
|
||||
Proxy: tt.fields.Proxy,
|
||||
}
|
||||
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Init() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloaderStoreConfig_Merge(t *testing.T) {
|
||||
type args struct {
|
||||
beforeCfg *DownloaderStoreConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields *DownloaderStoreConfig
|
||||
args args
|
||||
want *DownloaderStoreConfig
|
||||
}{
|
||||
{
|
||||
"Merge Nil",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: nil,
|
||||
},
|
||||
&DownloaderStoreConfig{},
|
||||
},
|
||||
{
|
||||
"Merge DownloadDir No Override",
|
||||
&DownloaderStoreConfig{
|
||||
DownloadDir: "before",
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
DownloadDir: "after",
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
DownloadDir: "before",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge DownloadDir Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
DownloadDir: "after",
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
DownloadDir: "after",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge MaxRunning No Override",
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 1,
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge MaxRunning Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge ProtocolConfig No Override",
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge ProtocolConfig Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Extra No Override",
|
||||
&DownloaderStoreConfig{
|
||||
Extra: map[string]any{},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Extra: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Extra: map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Extra Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Extra: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Extra: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Proxy No Override",
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Scheme: "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Proxy Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Scheme: "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Scheme: "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &DownloaderStoreConfig{
|
||||
FirstLoad: tt.fields.FirstLoad,
|
||||
DownloadDir: tt.fields.DownloadDir,
|
||||
MaxRunning: tt.fields.MaxRunning,
|
||||
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||
Extra: tt.fields.Extra,
|
||||
Proxy: tt.fields.Proxy,
|
||||
}
|
||||
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Merge() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+244
-123
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
"github.com/virtuald/go-paniclog"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -42,25 +44,29 @@ var (
|
||||
type Listener func(event *Event)
|
||||
|
||||
type Progress struct {
|
||||
// 下载耗时(纳秒)
|
||||
// Total download time(ns)
|
||||
Used int64 `json:"used"`
|
||||
// 每秒下载字节数
|
||||
// Download speed(bytes/s)
|
||||
Speed int64 `json:"speed"`
|
||||
// 已下载的字节数
|
||||
// Downloaded size(bytes)
|
||||
Downloaded int64 `json:"downloaded"`
|
||||
// Uploaded speed(bytes/s)
|
||||
UploadSpeed int64 `json:"uploadSpeed"`
|
||||
// Uploaded size(bytes)
|
||||
Uploaded int64 `json:"uploaded"`
|
||||
}
|
||||
|
||||
type Downloader struct {
|
||||
Logger *logger.Logger
|
||||
ExtensionLogger *logger.Logger
|
||||
|
||||
cfg *DownloaderConfig
|
||||
fetcherBuilders map[string]fetcher.FetcherBuilder
|
||||
fetcherCache map[string]fetcher.Fetcher
|
||||
storage Storage
|
||||
tasks []*Task
|
||||
waitTasks []*Task
|
||||
listener Listener
|
||||
cfg *DownloaderConfig
|
||||
fetcherCache map[string]fetcher.Fetcher
|
||||
storage Storage
|
||||
tasks []*Task
|
||||
waitTasks []*Task
|
||||
watchedTasks sync.Map
|
||||
listener Listener
|
||||
|
||||
lock *sync.Mutex
|
||||
fetcherMapLock *sync.RWMutex
|
||||
@@ -77,11 +83,10 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
||||
cfg.Init()
|
||||
|
||||
d := &Downloader{
|
||||
cfg: cfg,
|
||||
fetcherBuilders: make(map[string]fetcher.FetcherBuilder),
|
||||
fetcherCache: make(map[string]fetcher.Fetcher),
|
||||
waitTasks: make([]*Task, 0),
|
||||
storage: cfg.Storage,
|
||||
cfg: cfg,
|
||||
fetcherCache: make(map[string]fetcher.Fetcher),
|
||||
waitTasks: make([]*Task, 0),
|
||||
storage: cfg.Storage,
|
||||
|
||||
lock: &sync.Mutex{},
|
||||
fetcherMapLock: &sync.RWMutex{},
|
||||
@@ -89,11 +94,6 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
||||
|
||||
extensions: make([]*Extension, 0),
|
||||
}
|
||||
for _, f := range cfg.FetchBuilders {
|
||||
for _, p := range f.Schemes() {
|
||||
d.fetcherBuilders[strings.ToUpper(p)] = f
|
||||
}
|
||||
}
|
||||
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
|
||||
@@ -110,7 +110,7 @@ func (d *Downloader) Setup() error {
|
||||
return err
|
||||
}
|
||||
// load config from storage
|
||||
var cfg DownloaderStoreConfig
|
||||
var cfg base.DownloaderStoreConfig
|
||||
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -118,17 +118,20 @@ func (d *Downloader) Setup() error {
|
||||
if exist {
|
||||
d.cfg.DownloaderStoreConfig = &cfg
|
||||
} else {
|
||||
d.cfg.DownloaderStoreConfig = &DownloaderStoreConfig{
|
||||
d.cfg.DownloaderStoreConfig = &base.DownloaderStoreConfig{
|
||||
FirstLoad: true,
|
||||
}
|
||||
}
|
||||
// init default config
|
||||
d.cfg.DownloaderStoreConfig.Init()
|
||||
for _, fb := range d.fetcherBuilders {
|
||||
f := fb.Build()
|
||||
d.setupFetcher(f)
|
||||
|
||||
// init protocol config, if not exist, use default config
|
||||
for _, fm := range d.cfg.FetchManagers {
|
||||
protocol := fm.Name()
|
||||
if _, ok := d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol]; !ok {
|
||||
d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol] = fm.DefaultConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// load tasks from storage
|
||||
var tasks []*Task
|
||||
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
||||
@@ -166,6 +169,22 @@ func (d *Downloader) Setup() error {
|
||||
}
|
||||
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
|
||||
go func() {
|
||||
for !d.closed.Load() {
|
||||
@@ -174,22 +193,31 @@ func (d *Downloader) Setup() error {
|
||||
func() {
|
||||
task.statusLock.Lock()
|
||||
defer task.statusLock.Unlock()
|
||||
if task.Status != base.DownloadStatusRunning {
|
||||
if task.Status != base.DownloadStatusRunning && !task.Uploading {
|
||||
return
|
||||
}
|
||||
// check if task is deleted
|
||||
if d.GetTask(task.ID) == nil {
|
||||
if d.GetTask(task.ID) == nil || task.fetcher == nil {
|
||||
return
|
||||
}
|
||||
|
||||
current := task.fetcher.Progress().TotalDownloaded()
|
||||
task.Progress.Used = task.timer.Used()
|
||||
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000)
|
||||
task.Progress.Downloaded = current
|
||||
tick := float64(d.cfg.RefreshInterval) / 1000
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
task.Progress.Used = task.timer.Used()
|
||||
task.Progress.Speed = task.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick)
|
||||
task.Progress.Downloaded = current
|
||||
}
|
||||
if task.Uploading {
|
||||
uploader := task.fetcher.(fetcher.Uploader)
|
||||
currentUploaded := uploader.UploadedBytes()
|
||||
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
|
||||
task.Progress.Uploaded = currentUploaded
|
||||
}
|
||||
d.emit(EventKeyProgress, task)
|
||||
|
||||
// store fetcher progress
|
||||
data, err := task.fetcherBuilder.Store(task.fetcher)
|
||||
data, err := task.fetcherManager.Store(task.fetcher)
|
||||
if err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
||||
return
|
||||
@@ -211,21 +239,36 @@ func (d *Downloader) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
|
||||
schema := util.ParseSchema(url)
|
||||
fetchBuilder, ok := d.fetcherBuilders[schema]
|
||||
if ok {
|
||||
return fetchBuilder, nil
|
||||
func (d *Downloader) parseFm(url string) (fetcher.FetcherManager, error) {
|
||||
for _, fm := range d.cfg.FetchManagers {
|
||||
for _, filter := range fm.Filters() {
|
||||
if filter.Match(url) {
|
||||
return fm, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrUnSupportedProtocol
|
||||
}
|
||||
|
||||
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
||||
func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fetcher) {
|
||||
ctl := controller.NewController()
|
||||
ctl.GetConfig = func(v any) bool {
|
||||
return d.getProtocolConfig(fetcher.Name(), v)
|
||||
ctl.GetConfig = func(v any) {
|
||||
d.getProtocolConfig(fm.Name(), v)
|
||||
}
|
||||
// Get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
if requestProxy == nil {
|
||||
return d.cfg.Proxy.ToHandler()
|
||||
}
|
||||
switch requestProxy.Mode {
|
||||
case base.RequestProxyModeNone:
|
||||
return nil
|
||||
case base.RequestProxyModeCustom:
|
||||
return requestProxy.ToHandler()
|
||||
default:
|
||||
return d.cfg.Proxy.ToHandler()
|
||||
}
|
||||
}
|
||||
ctl.ProxyUrl = d.cfg.ProxyUrl()
|
||||
fetcher.Setup(ctl)
|
||||
}
|
||||
|
||||
@@ -301,6 +344,18 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
|
||||
return d.doCreate(fetcher, opts)
|
||||
}
|
||||
|
||||
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
|
||||
taskIds := make([]string, 0)
|
||||
for _, req := range reqs {
|
||||
taskId, err := d.CreateDirect(req, opts.Clone())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
taskIds = append(taskIds, taskId)
|
||||
}
|
||||
return taskIds, nil
|
||||
}
|
||||
|
||||
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
|
||||
d.fetcherMapLock.RLock()
|
||||
fetcher, ok := d.fetcherCache[rrId]
|
||||
@@ -322,18 +377,10 @@ func (d *Downloader) Pause(id string) (err error) {
|
||||
return ErrTaskNotFound
|
||||
}
|
||||
|
||||
func() {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
if task.Status == base.DownloadStatusPause {
|
||||
return
|
||||
}
|
||||
if err = d.doPause(task); err != nil {
|
||||
return
|
||||
}
|
||||
d.notifyRunning()
|
||||
}()
|
||||
if err = d.doPause(task); err != nil {
|
||||
return
|
||||
}
|
||||
d.notifyRunning()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -452,8 +499,10 @@ func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err err
|
||||
}
|
||||
|
||||
deleteIds := make([]string, 0)
|
||||
deleteTasksPtr := make([]*Task, 0)
|
||||
for _, task := range deleteTasks {
|
||||
deleteIds = append(deleteIds, task.ID)
|
||||
deleteTasksPtr = append(deleteTasksPtr, task)
|
||||
}
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
@@ -475,7 +524,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)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -491,6 +540,17 @@ func (d *Downloader) Stats(id string) (sr any, err error) {
|
||||
if task == nil {
|
||||
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()
|
||||
return
|
||||
}
|
||||
@@ -536,10 +596,23 @@ func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
||||
|
||||
func (d *Downloader) Close() error {
|
||||
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 {
|
||||
@@ -603,11 +676,11 @@ func (d *Downloader) GetTasksByStatues(statues []base.Status) []*Task {
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
|
||||
func (d *Downloader) GetConfig() (*base.DownloaderStoreConfig, error) {
|
||||
return d.cfg.DownloaderStoreConfig, nil
|
||||
}
|
||||
|
||||
func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
|
||||
func (d *Downloader) PutConfig(v *base.DownloaderStoreConfig) error {
|
||||
d.cfg.DownloaderStoreConfig = v
|
||||
return d.storage.Put(bucketConfig, "config", v)
|
||||
}
|
||||
@@ -629,11 +702,49 @@ func (d *Downloader) getProtocolConfig(name string, v any) bool {
|
||||
|
||||
// wait task done
|
||||
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()
|
||||
if err != nil {
|
||||
d.doOnError(task, err)
|
||||
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()
|
||||
if task.Meta.Res.Size == 0 {
|
||||
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
||||
@@ -650,6 +761,7 @@ func (d *Downloader) watch(task *Task) {
|
||||
d.emit(EventKeyDone, task)
|
||||
d.emit(EventKeyFinally, task, err)
|
||||
d.notifyRunning()
|
||||
d.triggerOnDone(task)
|
||||
|
||||
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
||||
downloadFilePath := task.Meta.SingleFilepath()
|
||||
@@ -683,46 +795,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 {
|
||||
fb, err := d.parseFb(task.Meta.Req.URL)
|
||||
if err != nil {
|
||||
if err := d.restoreFetcher(task); err != nil {
|
||||
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)
|
||||
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 {
|
||||
opts = &base.Options{}
|
||||
}
|
||||
@@ -730,7 +847,7 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
opts.SelectFiles = make([]int, 0)
|
||||
}
|
||||
|
||||
meta := fetcher.Meta()
|
||||
meta := f.Meta()
|
||||
meta.Opts = opts
|
||||
if opts.Path == "" {
|
||||
storeConfig, err := d.GetConfig()
|
||||
@@ -740,19 +857,20 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
opts.Path = storeConfig.DownloadDir
|
||||
}
|
||||
|
||||
fb, err := d.parseFb(fetcher.Meta().Req.URL)
|
||||
fm, err := d.parseFm(f.Meta().Req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task := NewTask()
|
||||
task.fetcherBuilder = fb
|
||||
task.fetcher = fetcher
|
||||
task.Protocol = fetcher.Name()
|
||||
task.Meta = fetcher.Meta()
|
||||
task.fetcherManager = fm
|
||||
task.fetcher = f
|
||||
task.Protocol = fm.Name()
|
||||
task.Meta = f.Meta()
|
||||
task.Progress = &Progress{}
|
||||
_, task.Uploading = f.(fetcher.Uploader)
|
||||
initTask(task)
|
||||
if err = fetcher.Create(opts); err != nil {
|
||||
if err = f.Create(opts); err != nil {
|
||||
return
|
||||
}
|
||||
if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
@@ -795,7 +913,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = d.restoreFetcher(task)
|
||||
err = d.restoreTask(task)
|
||||
if err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
||||
return
|
||||
@@ -828,26 +946,28 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
}
|
||||
|
||||
if isCreate {
|
||||
d.checkDuplicateLock.Lock()
|
||||
defer d.checkDuplicateLock.Unlock()
|
||||
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
||||
// check if the download file is duplicated and rename it automatically.
|
||||
if task.Meta.Res.Name != "" {
|
||||
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
|
||||
fullDirPath := task.Meta.FolderPath()
|
||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
if task.fetcherManager.AutoRename() {
|
||||
d.checkDuplicateLock.Lock()
|
||||
defer d.checkDuplicateLock.Unlock()
|
||||
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
||||
// check if the download file is duplicated and rename it automatically.
|
||||
if task.Meta.Res.Name != "" {
|
||||
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
|
||||
fullDirPath := task.Meta.FolderPath()
|
||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||
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)
|
||||
@@ -855,10 +975,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
|
||||
task.Progress.Speed = 0
|
||||
task.timer.Start()
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
if err := task.fetcher.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := task.fetcher.Start(); err != nil {
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
return err
|
||||
}
|
||||
d.emit(EventKeyStart, task)
|
||||
@@ -928,12 +1048,12 @@ func logPanic(logDir string) {
|
||||
}
|
||||
|
||||
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||
fb, err := d.parseFb(url)
|
||||
fm, err := d.parseFm(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fetcher := fb.Build()
|
||||
d.setupFetcher(fetcher)
|
||||
fetcher := fm.Build()
|
||||
d.setupFetcher(fm, fetcher)
|
||||
return fetcher, nil
|
||||
}
|
||||
|
||||
@@ -943,6 +1063,7 @@ func initTask(task *Task) {
|
||||
task.statusLock = &sync.Mutex{}
|
||||
task.lock = &sync.Mutex{}
|
||||
task.speedArr = make([]int64, 0)
|
||||
task.uploadSpeedArr = make([]int64, 0)
|
||||
}
|
||||
|
||||
var defaultDownloader = NewDownloader(nil)
|
||||
|
||||
+167
-39
@@ -4,6 +4,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -83,39 +86,148 @@ func TestDownloader_Create(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||
// No proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return nil
|
||||
func TestDownloader_CreateDirectBatch(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
defer listener.Close()
|
||||
|
||||
downloader := NewDownloader(nil)
|
||||
if err := downloader.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
downloader.DeleteByStatues(nil, true)
|
||||
downloader.Clear()
|
||||
}()
|
||||
|
||||
reqs := make([]*base.Request, 0)
|
||||
fileNames := make([]string, 0)
|
||||
for i := 0; i < 5; i++ {
|
||||
req := &base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
reqs = append(reqs, req)
|
||||
if i == 0 {
|
||||
fileNames = append(fileNames, test.DownloadName)
|
||||
} else {
|
||||
arr := strings.Split(test.DownloadName, ".")
|
||||
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(reqs))
|
||||
downloader.Listener(func(event *Event) {
|
||||
if event.Key == EventKeyDone {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
// Disable proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Enable = false
|
||||
return proxyCfg
|
||||
})
|
||||
// Invalid proxy scheme
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Scheme = ""
|
||||
return proxyCfg
|
||||
})
|
||||
// Invalid proxy host
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Host = ""
|
||||
return proxyCfg
|
||||
})
|
||||
// Use proxy without auth
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
})
|
||||
// Use proxy with auth
|
||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
|
||||
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Extra: http.OptsExtra{
|
||||
Connections: 4,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
tasks := downloader.GetTasks()
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("CreateDirectBatch() task got = %v, want %v", len(tasks), len(reqs))
|
||||
}
|
||||
|
||||
nameMatchMap := make(map[string]bool)
|
||||
for _, name := range fileNames {
|
||||
if _, err := os.Stat(test.Dir + "/" + name); os.IsNotExist(err) {
|
||||
t.Errorf("CreateDirectBatch() file not exist: %v", name)
|
||||
}
|
||||
for _, task := range tasks {
|
||||
if name == task.Meta.Opts.Name {
|
||||
nameMatchMap[task.Meta.Opts.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(nameMatchMap) != len(reqs) {
|
||||
t.Errorf("CreateDirectBatch() names got = %v, want %v", len(nameMatchMap), len(reqs))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig) {
|
||||
httpListener := test.StartTestFileServer()
|
||||
defer httpListener.Close()
|
||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||
// No proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
return nil
|
||||
}, nil)
|
||||
// Disable proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.Enable = false
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Enable system proxy but not set proxy environment variable
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.System = true
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Enable proxy but error proxy environment variable
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
||||
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
||||
proxyCfg.System = true
|
||||
return proxyCfg
|
||||
}, func(err error) {
|
||||
if err == nil {
|
||||
t.Fatal("doTestDownloaderCreateWithProxy() got = nil, want error")
|
||||
}
|
||||
})
|
||||
// Enable system proxy and set proxy environment variable
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
||||
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
||||
proxyCfg.System = true
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Invalid proxy scheme
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.Scheme = ""
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Invalid proxy host
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.Host = ""
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Use proxy without auth
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Use proxy with auth
|
||||
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
|
||||
// Request proxy mode follow
|
||||
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||
reqProxy.Mode = base.RequestProxyModeFollow
|
||||
return reqProxy
|
||||
}, nil, nil)
|
||||
|
||||
// Request proxy mode none
|
||||
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||
reqProxy.Mode = base.RequestProxyModeNone
|
||||
return reqProxy
|
||||
}, nil, nil)
|
||||
|
||||
// Request proxy mode custom
|
||||
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||
return reqProxy
|
||||
}, nil, nil)
|
||||
}
|
||||
|
||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
||||
usr, pwd := "", ""
|
||||
if auth {
|
||||
usr, pwd = "admin", "123"
|
||||
@@ -128,29 +240,45 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer downloader.Clear()
|
||||
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&DownloaderProxyConfig{
|
||||
globalProxyCfg := &base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
})
|
||||
}
|
||||
if buildProxyConfig != nil {
|
||||
globalProxyCfg = buildProxyConfig(globalProxyCfg)
|
||||
}
|
||||
downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg
|
||||
|
||||
req := &base.Request{
|
||||
URL: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
|
||||
URL: test.ExternalDownloadUrl,
|
||||
}
|
||||
if buildReqProxy != nil {
|
||||
req.Proxy = buildReqProxy(&base.RequestProxy{
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
})
|
||||
}
|
||||
rr, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if errHandler == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errHandler(err)
|
||||
return
|
||||
}
|
||||
want := &base.Resource{
|
||||
Size: test.BuildSize,
|
||||
Size: test.ExternalDownloadSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
Name: test.ExternalDownloadName,
|
||||
Path: "",
|
||||
Size: test.BuildSize,
|
||||
Size: test.ExternalDownloadSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -281,11 +409,11 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
||||
|
||||
var httpCfg map[string]any
|
||||
exits := downloader.getProtocolConfig("http", &httpCfg)
|
||||
if exits {
|
||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
||||
if !exits {
|
||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, true)
|
||||
}
|
||||
|
||||
storeCfg := &DownloaderStoreConfig{
|
||||
storeCfg := &base.DownloaderStoreConfig{
|
||||
DownloadDir: "./downloads",
|
||||
ProtocolConfig: map[string]any{
|
||||
"http": map[string]any{
|
||||
|
||||
@@ -3,6 +3,7 @@ package engine
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"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"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
gojaurl "github.com/dop251/goja_nodejs/url"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -106,11 +106,11 @@ func (e *Engine) await(value any) {
|
||||
}
|
||||
|
||||
func (e *Engine) Close() {
|
||||
e.loop.Stop()
|
||||
e.loop.StopNoWait()
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ProxyURL *url.URL
|
||||
ProxyConfig *base.DownloaderProxyConfig
|
||||
}
|
||||
|
||||
func NewEngine(cfg *Config) *Engine {
|
||||
@@ -135,7 +135,7 @@ func NewEngine(cfg *Config) *Engine {
|
||||
if err := formdata.Enable(runtime); err != nil {
|
||||
return
|
||||
}
|
||||
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
|
||||
if err := xhr.Enable(runtime, cfg.ProxyConfig.ToHandler()); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := runtime.RunString(polyfillScript); err != nil {
|
||||
@@ -145,6 +145,10 @@ func NewEngine(cfg *Config) *Engine {
|
||||
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
||||
return
|
||||
}
|
||||
// polyfill window
|
||||
if err := runtime.Set("window", runtime.GlobalObject()); err != nil {
|
||||
return
|
||||
}
|
||||
// polyfill window.location
|
||||
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
||||
return
|
||||
@@ -174,7 +178,15 @@ func resolveResult(value goja.Value) (any, error) {
|
||||
if err, ok := p.Result().Export().(error); ok {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, errors.New(p.Result().String())
|
||||
stack := p.Result().String()
|
||||
result := p.Result()
|
||||
if ro, ok := result.(*goja.Object); ok {
|
||||
stackVal := ro.Get("stack")
|
||||
if stackVal != nil && stackVal.String() != "" {
|
||||
stack = stackVal.String()
|
||||
}
|
||||
}
|
||||
return nil, errors.New(stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/dop251/goja"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -19,6 +13,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"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) {
|
||||
@@ -26,6 +27,8 @@ func TestPolyfill(t *testing.T) {
|
||||
doTestPolyfill(t, "XMLHttpRequest")
|
||||
doTestPolyfill(t, "Blob")
|
||||
doTestPolyfill(t, "FormData")
|
||||
doTestPolyfill(t, "TextDecoder")
|
||||
doTestPolyfill(t, "TextEncoder")
|
||||
doTestPolyfill(t, "fetch")
|
||||
doTestPolyfill(t, "__gopeed_create_vm")
|
||||
}
|
||||
@@ -69,6 +72,44 @@ async function testOctetStream(file){
|
||||
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){
|
||||
const formData = new FormData();
|
||||
formData.append('name', 'test');
|
||||
@@ -80,6 +121,30 @@ async function testFormData(file){
|
||||
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(){
|
||||
return new Promise((resolve, reject) => {
|
||||
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() {
|
||||
jsFile, goFile, md5 := buildFile(t, engine.Runtime)
|
||||
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")
|
||||
if err != nil {
|
||||
t.Fatal("progress test failed")
|
||||
t.Fatal("progress test failed", err)
|
||||
}
|
||||
|
||||
_, err = callTestFun(engine, "testAbort")
|
||||
if err != nil {
|
||||
t.Fatal("abort test failed")
|
||||
t.Fatal("abort test failed", err)
|
||||
}
|
||||
|
||||
_, err = callTestFun(engine, "testTimeout")
|
||||
@@ -217,8 +301,16 @@ func doTestFetchWithProxy(t *testing.T, usr, pwd string) {
|
||||
|
||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||
defer proxyListener.Close()
|
||||
|
||||
engine := NewEngine(&Config{ProxyURL: util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)})
|
||||
engine := NewEngine(&Config{
|
||||
ProxyConfig: &base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
System: false,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
},
|
||||
})
|
||||
|
||||
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -320,6 +412,15 @@ func startServer() net.Listener {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
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) {
|
||||
buf, _ := io.ReadAll(r.Body)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -359,6 +460,16 @@ func startServer() net.Listener {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
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
|
||||
go server.Serve(listener)
|
||||
return listener
|
||||
|
||||
@@ -2,16 +2,19 @@ package xhr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
"errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"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 (
|
||||
@@ -23,6 +26,12 @@ const (
|
||||
eventTimeout = "timeout"
|
||||
)
|
||||
|
||||
const (
|
||||
redirectError = "error"
|
||||
redirectFollow = "follow"
|
||||
redirectManual = "manual"
|
||||
)
|
||||
|
||||
type ProgressEvent struct {
|
||||
Type string `json:"type"`
|
||||
LengthComputable bool `json:"lengthComputable"`
|
||||
@@ -117,18 +126,26 @@ type XMLHttpRequestUpload struct {
|
||||
type XMLHttpRequest struct {
|
||||
method string
|
||||
url string
|
||||
requestHeaders map[string]string
|
||||
responseHeaders map[string]string
|
||||
requestHeaders http.Header
|
||||
responseHeaders http.Header
|
||||
aborted bool
|
||||
proxyUrl *url.URL
|
||||
proxyHandler func(r *http.Request) (*url.URL, error)
|
||||
|
||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||
Timeout int `json:"timeout"`
|
||||
ReadyState int `json:"readyState"`
|
||||
Status int `json:"status"`
|
||||
StatusText string `json:"statusText"`
|
||||
Response string `json:"response"`
|
||||
ResponseText string `json:"responseText"`
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||
Timeout int `json:"timeout"`
|
||||
ReadyState int `json:"readyState"`
|
||||
Status int `json:"status"`
|
||||
StatusText string `json:"statusText"`
|
||||
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
|
||||
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
|
||||
}
|
||||
@@ -136,13 +153,13 @@ type XMLHttpRequest struct {
|
||||
func (xhr *XMLHttpRequest) Open(method, url string) {
|
||||
xhr.method = method
|
||||
xhr.url = url
|
||||
xhr.requestHeaders = make(map[string]string)
|
||||
xhr.responseHeaders = make(map[string]string)
|
||||
xhr.requestHeaders = make(http.Header)
|
||||
xhr.responseHeaders = make(http.Header)
|
||||
xhr.doReadystatechange(1)
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
||||
xhr.requestHeaders[key] = value
|
||||
xhr.requestHeaders.Add(key, value)
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
@@ -152,31 +169,33 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
var (
|
||||
contentType string
|
||||
contentLength int64
|
||||
isStringBody bool
|
||||
)
|
||||
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
||||
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
||||
} else {
|
||||
switch d.(type) {
|
||||
switch v := d.(type) {
|
||||
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"
|
||||
contentLength = int64(len(d.(string)))
|
||||
contentLength = int64(len(v))
|
||||
isStringBody = true
|
||||
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"
|
||||
contentLength = d.(*file.File).Size
|
||||
contentLength = v.Size
|
||||
case *formdata.FormData:
|
||||
pr, pw := io.Pipe()
|
||||
mw := NewMultipart(pw)
|
||||
for _, e := range d.(*formdata.FormData).Entries() {
|
||||
for _, e := range v.Entries() {
|
||||
arr := e.([]any)
|
||||
k := arr[0].(string)
|
||||
v := arr[1]
|
||||
switch v.(type) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
mw.WriteField(k, v.(string))
|
||||
mw.WriteField(k, v)
|
||||
case *file.File:
|
||||
mw.WriteFile(k, v.(*file.File))
|
||||
mw.WriteFile(k, v)
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
@@ -193,27 +212,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
xhr.callOnerror()
|
||||
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)
|
||||
}
|
||||
if contentLength > 0 {
|
||||
req.ContentLength = contentLength
|
||||
}
|
||||
for k, v := range xhr.requestHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
transport := &http.Transport{}
|
||||
if xhr.proxyUrl != nil {
|
||||
transport.Proxy = http.ProxyURL(xhr.proxyUrl)
|
||||
transport := &http.Transport{
|
||||
Proxy: xhr.proxyHandler,
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
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)
|
||||
if err != nil {
|
||||
// handle timeout error
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
if xhr.Timeout > 0 {
|
||||
xhr.Upload.callOntimeout()
|
||||
xhr.callOntimeout()
|
||||
@@ -229,9 +259,12 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
if !xhr.aborted {
|
||||
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.StatusText = resp.Status
|
||||
xhr.doReadystatechange(2)
|
||||
@@ -260,7 +293,7 @@ func (xhr *XMLHttpRequest) Abort() {
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
||||
return xhr.responseHeaders[key]
|
||||
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
||||
@@ -268,7 +301,7 @@ func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
||||
for k, v := range xhr.responseHeaders {
|
||||
buf.WriteString(k)
|
||||
buf.WriteString(": ")
|
||||
buf.WriteString(v)
|
||||
buf.WriteString(strings.Join(v, ", "))
|
||||
buf.WriteString("\r\n")
|
||||
}
|
||||
return buf.String()
|
||||
@@ -313,7 +346,7 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
|
||||
return data.String()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
||||
func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL, error)) error {
|
||||
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
if len(call.Arguments) < 1 {
|
||||
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
|
||||
@@ -327,7 +360,7 @@ func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
||||
})
|
||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
instance := &XMLHttpRequest{
|
||||
proxyUrl: proxyUrl,
|
||||
proxyHandler: proxyHandler,
|
||||
Upload: &XMLHttpRequestUpload{
|
||||
EventProp: &EventProp{
|
||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||
@@ -386,10 +419,9 @@ func (w *multipartWrapper) Size() int64 {
|
||||
w.statWriter.Close()
|
||||
size := int64(w.statBuffer.Len())
|
||||
for _, v := range w.fields {
|
||||
switch v.(type) {
|
||||
switch v := v.(type) {
|
||||
case *file.File:
|
||||
f := v.(*file.File)
|
||||
size += f.Size
|
||||
size += v.Size
|
||||
}
|
||||
}
|
||||
return size
|
||||
@@ -397,18 +429,17 @@ func (w *multipartWrapper) Size() int64 {
|
||||
|
||||
func (w *multipartWrapper) Send() error {
|
||||
for k, v := range w.fields {
|
||||
switch v.(type) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
if err := w.writer.WriteField(k, v.(string)); err != nil {
|
||||
if err := w.writer.WriteField(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case *file.File:
|
||||
f := v.(*file.File)
|
||||
fw, err := w.writer.CreateFormFile(k, f.Name)
|
||||
fw, err := w.writer.CreateFormFile(k, v.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = io.Copy(fw, f); err != nil {
|
||||
if _, err = io.Copy(fw, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e<r;){var u=t.charCodeAt(e++);if(u>=55296&&u<=56319){if(e<r){var f=t.charCodeAt(e);56320==(64512&f)&&(++e,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o<e;){var n,i,a,s,u=t[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p<c;)h+=String.fromCharCode.apply(String,r.slice(p,p+=4096));return h};function b(){var e=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;e&&r&&(XMLHttpRequest.prototype.send=function(t){t instanceof Blob?(this.setRequestHeader("Content-Type",t.type),r.call(this,t)):r.call(this,t)});try{new File([],""),t.File=i.File,t.FileReader=i.FileReader}catch(e){try{t.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(e){t.File=function(t,e,r){var o=new Blob(t,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=e.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),t.Blob=f?i.Blob:p):l?(b(),t.Blob=h):function(){function e(t){for(var e=new Array(t.byteLength),r=new Uint8Array(t),o=e.length;o--;)e[o]=r[o];return e}function o(t){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<t.length;o+=3){var n=t[o],i=o+1<t.length,a=i?t[o+1]:0,s=o+2<t.length,u=s?t[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o<n;o++){var a=t[o];a instanceof h?t[o]=a._buffer:"string"==typeof a?t[o]=y(a):"DataView"===u(s=a)||f(i.DataView,s)?t[o]=e(a.buffer):c(a)?t[o]=e(a):t[o]=y(String(a))}var s;this._buffer=i.Uint8Array?function(t){for(var e=0,r=t.length;r--;)e+=t[r].length;for(var o=new Uint8Array(e),n=0,i=0;i<t.length;i++){var a=t[i];o.set(a,n),n+=a.byteLength||a.length}return o}(t):[].concat.apply([],t),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
||||
(()=>{var e={725:function(e,t,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(e){"use strict";var t=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(e,t){return(t=document.createElement("a")).href=e,t},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=t&&t.prototype.append&&t.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(e){}function c(e){return e.map((function(e){if(e.buffer instanceof ArrayBuffer){var t=e.buffer;if(e.byteLength!==t.byteLength){var r=new Uint8Array(e.byteLength);r.set(new Uint8Array(t,e.byteOffset,e.byteLength)),t=r.buffer}return t}return e}))}function h(e,r){r=r||{};var o=new t;return c(e).forEach((function(e){o.append(e)})),r.type?o.getBlob(r.type):o.getBlob()}function d(e,t){return new o(c(e),t||{})}i.Blob&&(h.prototype=Blob.prototype,d.prototype=Blob.prototype);var p="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(e){for(var t=0,r=e.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);t<r;){var u=e.charCodeAt(t++);if(u>=55296&&u<=56319){if(t<r){var f=e.charCodeAt(t);56320==(64512&f)&&(++t,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+t/e.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},y="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(e){for(var t=e.length,r=[],o=0;o<t;){var n,i,a,s,u=e[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=t)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=e[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=e[o+1],i=e[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=e[o+1],i=e[o+2],a=e[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",d=0;d<c;)h+=String.fromCharCode.apply(String,r.slice(d,d+=4096));return h};function b(){var t=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;t&&r&&(XMLHttpRequest.prototype.send=function(e){e instanceof Blob?(this.setRequestHeader("Content-Type",e.type),r.call(this,e)):r.call(this,e)});try{new File([],""),e.File=i.File,e.FileReader=i.FileReader}catch(t){try{e.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(t){e.File=function(e,t,r){var o=new Blob(e,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=t.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),e.Blob=f?i.Blob:d):l?(b(),e.Blob=h):function(){function t(e){for(var t=new Array(e.byteLength),r=new Uint8Array(e),o=t.length;o--;)t[o]=r[o];return t}function o(e){for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<e.length;o+=3){var n=e[o],i=o+1<e.length,a=i?e[o+1]:0,s=o+2<e.length,u=s?e[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(t[f],t[l],t[c],t[h])}return r.join("")}var s=Object.create||function(e){function t(){}return t.prototype=e,new t};function u(e){return Object.prototype.toString.call(e).slice(8,-1)}function f(e,t){return"object"==typeof e&&Object.prototype.isPrototypeOf.call(e.prototype,t)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(e){return t=l,r=u(e),-1!==t.indexOf(r)||f(i.ArrayBuffer,e);var t,r}function h(e,r){r=null==r?{}:r;for(var o=0,n=(e=e?e.slice():[]).length;o<n;o++){var a=e[o];a instanceof h?e[o]=a._buffer:"string"==typeof a?e[o]=p(a):"DataView"===u(s=a)||f(i.DataView,s)?e[o]=t(a.buffer):c(a)?e[o]=t(a):e[o]=p(String(a))}var s;this._buffer=i.Uint8Array?function(e){for(var t=0,r=e.length;r--;)t+=e[r].length;for(var o=new Uint8Array(t),n=0,i=0;i<e.length;i++){var a=e[i];o.set(a,n),n+=a.byteLength||a.length}return o}(e):[].concat.apply([],e),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
||||
+1169
-15
File diff suppressed because it is too large.
Load diff
@@ -7,16 +7,19 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "webpack --mode production --watch",
|
||||
"build": "webpack --mode production"
|
||||
"build": "webpack --mode production",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"patch-package": "^8.0.0",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"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 "./crypto/index.js"
|
||||
// polyfill TextEncoder
|
||||
import 'fastestsmallesttextencoderdecoder';
|
||||
import "./fetch/index.js"
|
||||
@@ -38,7 +38,7 @@ const (
|
||||
EventOnResolve ActivationEvent = "onResolve"
|
||||
EventOnStart ActivationEvent = "onStart"
|
||||
EventOnError ActivationEvent = "onError"
|
||||
//EventOnDone ActivationEvent = "onDone"
|
||||
EventOnDone ActivationEvent = "onDone"
|
||||
)
|
||||
|
||||
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
||||
@@ -155,7 +155,13 @@ func (d *Downloader) DeleteExtension(identity string) error {
|
||||
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 {
|
||||
@@ -199,7 +205,7 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
|
||||
return nil, err
|
||||
}
|
||||
proxyOptions := transport.ProxyOptions{}
|
||||
proxyUrl := d.cfg.ProxyUrl()
|
||||
proxyUrl := d.cfg.DownloaderStoreConfig.Proxy.ToUrl()
|
||||
if proxyUrl != nil {
|
||||
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
|
||||
proxyOptions.Username = proxyUrl.User.Username()
|
||||
@@ -298,6 +304,17 @@ func (d *Downloader) triggerOnError(task *Task, err error) {
|
||||
)
|
||||
}
|
||||
|
||||
func (d *Downloader) triggerOnDone(task *Task) {
|
||||
doTrigger(d,
|
||||
EventOnDone,
|
||||
task.Meta.Req,
|
||||
&OnErrorContext{
|
||||
Task: NewExtensionTask(d, task),
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
||||
// init extension global object
|
||||
gopeed := &Instance{
|
||||
@@ -341,7 +358,7 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
|
||||
req.Labels = make(map[string]string)
|
||||
}
|
||||
engine := engine.NewEngine(&engine.Config{
|
||||
ProxyURL: d.cfg.ProxyUrl(),
|
||||
ProxyConfig: d.cfg.Proxy,
|
||||
})
|
||||
defer engine.Close()
|
||||
err = engine.Runtime.Set("gopeed", gopeed)
|
||||
@@ -605,9 +622,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
|
||||
h.register(EventOnError, fn)
|
||||
}
|
||||
|
||||
//func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||
// h.register(HookEventOnDone, fn)
|
||||
//}
|
||||
func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||
h.register(EventOnDone, fn)
|
||||
}
|
||||
|
||||
type ExtensionInfo struct {
|
||||
Identity string `json:"identity"`
|
||||
@@ -681,6 +698,10 @@ type OnErrorContext struct {
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
type OnDoneContext struct {
|
||||
Task *Task `json:"task"`
|
||||
}
|
||||
|
||||
type ExtensionTask struct {
|
||||
*Task
|
||||
|
||||
@@ -688,8 +709,12 @@ type ExtensionTask struct {
|
||||
}
|
||||
|
||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||
newTask := task.clone()
|
||||
// Assign the pointer of the properties that the extension supports modification
|
||||
newTask.Meta = task.Meta
|
||||
newTask.Status = task.Status
|
||||
return &ExtensionTask{
|
||||
Task: task.clone(),
|
||||
Task: newTask,
|
||||
download: download,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,8 +247,45 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
||||
}
|
||||
// extension on error modify url and continue download
|
||||
task := downloader.GetTask(id)
|
||||
if task.Meta.Req.URL != "https://github.com" {
|
||||
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
||||
if task.Status != base.DownloadStatusDone {
|
||||
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 {
|
||||
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||
|
||||
+59
-63
@@ -1,15 +1,14 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -21,19 +20,21 @@ type ResolveResult struct {
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Protocol string `json:"protocol"`
|
||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||
Status base.Status `json:"status"`
|
||||
Protocol string `json:"protocol"`
|
||||
Uploading bool `json:"uploading"`
|
||||
Progress *Progress `json:"progress"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
fetcherBuilder fetcher.FetcherBuilder
|
||||
fetcherManager fetcher.FetcherManager
|
||||
fetcher fetcher.Fetcher
|
||||
timer *util.Timer
|
||||
statusLock *sync.Mutex
|
||||
lock *sync.Mutex
|
||||
speedArr []int64
|
||||
uploadSpeedArr []int64
|
||||
}
|
||||
|
||||
func NewTask() *Task {
|
||||
@@ -49,37 +50,71 @@ 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) {
|
||||
t.UpdatedAt = time.Now()
|
||||
t.Status = status
|
||||
}
|
||||
|
||||
func (t *Task) clone() *Task {
|
||||
return &Task{
|
||||
ID: t.ID,
|
||||
Meta: t.Meta,
|
||||
Status: t.Status,
|
||||
Progress: t.Progress,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
return util.DeepClone(t)
|
||||
}
|
||||
|
||||
func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
|
||||
t.speedArr = append(t.speedArr, downloaded)
|
||||
if len(t.speedArr) > 6 {
|
||||
t.speedArr = t.speedArr[1:]
|
||||
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
||||
speedArr = append(speedArr, downloaded)
|
||||
if len(speedArr) > 6 {
|
||||
speedArr = speedArr[1:]
|
||||
}
|
||||
var total int64
|
||||
for _, v := range t.speedArr {
|
||||
for _, v := range speedArr {
|
||||
total += v
|
||||
}
|
||||
return int64(float64(total) / float64(len(t.speedArr)) / usedTime)
|
||||
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
||||
}
|
||||
|
||||
type DownloaderConfig struct {
|
||||
Controller *controller.Controller
|
||||
FetchBuilders []fetcher.FetcherBuilder
|
||||
FetchManagers []fetcher.FetcherManager
|
||||
|
||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||
Storage Storage
|
||||
@@ -87,17 +122,17 @@ type DownloaderConfig struct {
|
||||
|
||||
ProductionMode bool
|
||||
|
||||
*DownloaderStoreConfig
|
||||
*base.DownloaderStoreConfig
|
||||
}
|
||||
|
||||
func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
||||
if cfg.Controller == nil {
|
||||
cfg.Controller = controller.NewController()
|
||||
}
|
||||
if len(cfg.FetchBuilders) == 0 {
|
||||
cfg.FetchBuilders = []fetcher.FetcherBuilder{
|
||||
new(http.FetcherBuilder),
|
||||
new(bt.FetcherBuilder),
|
||||
if len(cfg.FetchManagers) == 0 {
|
||||
cfg.FetchManagers = []fetcher.FetcherManager{
|
||||
new(http.FetcherManager),
|
||||
new(bt.FetcherManager),
|
||||
}
|
||||
}
|
||||
if cfg.RefreshInterval == 0 {
|
||||
@@ -108,42 +143,3 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||
type DownloaderStoreConfig struct {
|
||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||
|
||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||
Extra map[string]any `json:"extra"`
|
||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.MaxRunning == 0 {
|
||||
cfg.MaxRunning = 5
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) ProxyUrl() *url.URL {
|
||||
if cfg.Proxy == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.Proxy.Enable == false || cfg.Proxy.Scheme == "" || cfg.Proxy.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return util.BuildProxyUrl(cfg.Proxy.Scheme, cfg.Proxy.Host, cfg.Proxy.Usr, cfg.Proxy.Pwd)
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
Scheme string `json:"scheme"`
|
||||
Host string `json:"host"`
|
||||
Usr string `json:"usr"`
|
||||
Pwd string `json:"pwd"`
|
||||
}
|
||||
@@ -69,7 +69,9 @@ func (n *MemStorage) Setup(buckets []string) error {
|
||||
func (n *MemStorage) Put(bucket string, key string, v any) error {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
memData[bucket][key] = v
|
||||
if bucketData, ok := memData[bucket]; ok {
|
||||
bucketData[key] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
gopeed.events.onDone(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
ctx.task.meta.req.labels['modified'] = 'true';
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "on-done",
|
||||
"title": "gopeed extension on done event test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onDone",
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://github.com/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,6 @@ gopeed.events.onError(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
gopeed.logger.info("error", ctx.error);
|
||||
ctx.task.meta.req.url = "https://github.com";
|
||||
ctx.task.pause();
|
||||
ctx.task.continue();
|
||||
});
|
||||
|
||||
@@ -4,11 +4,16 @@ type ReqExtra struct {
|
||||
Trackers []string `json:"trackers"`
|
||||
}
|
||||
|
||||
// Stats for download
|
||||
// Stats for torrent
|
||||
type Stats struct {
|
||||
// bt stats
|
||||
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
||||
TotalPeers int `json:"totalPeers"`
|
||||
ActivePeers int `json:"activePeers"`
|
||||
ConnectedSeeders int `json:"connectedSeeders"`
|
||||
// Total seed bytes
|
||||
SeedBytes int64 `json:"seedBytes"`
|
||||
// Seed ratio
|
||||
SeedRatio float64 `json:"seedRatio"`
|
||||
// Total seed time
|
||||
SeedTime int64 `json:"seedTime"`
|
||||
}
|
||||
+30
-2
@@ -8,8 +8,20 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func Info(w http.ResponseWriter, r *http.Request) {
|
||||
info := map[string]any{
|
||||
"version": base.Version,
|
||||
"runtime": runtime.Version(),
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"inDocker": base.InDocker == "true",
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(info))
|
||||
}
|
||||
|
||||
func Resolve(w http.ResponseWriter, r *http.Request) {
|
||||
var req base.Request
|
||||
if ReadJson(r, w, &req) {
|
||||
@@ -45,6 +57,22 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTaskBatch
|
||||
if ReadJson(r, w, &req) {
|
||||
if len(req.Reqs) == 0 {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(taskIds))
|
||||
}
|
||||
}
|
||||
|
||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
@@ -142,7 +170,7 @@ func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func PutConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var cfg download.DownloaderStoreConfig
|
||||
var cfg base.DownloaderStoreConfig
|
||||
if ReadJson(r, w, &cfg) {
|
||||
if err := Downloader.PutConfig(&cfg); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
@@ -309,7 +337,7 @@ func writeError(w http.ResponseWriter, msg string) {
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func getServerConfig() *download.DownloaderStoreConfig {
|
||||
func getServerConfig() *base.DownloaderStoreConfig {
|
||||
cfg, _ := Downloader.GetConfig()
|
||||
return cfg
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package model
|
||||
|
||||
type Command struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Action string `json:"action"`
|
||||
Params any `json:"params"`
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
@@ -13,12 +14,13 @@ const (
|
||||
)
|
||||
|
||||
type StartConfig struct {
|
||||
Network string `json:"network"`
|
||||
Address string `json:"address"`
|
||||
RefreshInterval int `json:"refreshInterval"`
|
||||
Storage Storage `json:"storage"`
|
||||
StorageDir string `json:"storageDir"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
Network string `json:"network"`
|
||||
Address string `json:"address"`
|
||||
RefreshInterval int `json:"refreshInterval"`
|
||||
Storage Storage `json:"storage"`
|
||||
StorageDir string `json:"storageDir"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||
|
||||
ProductionMode bool
|
||||
|
||||
|
||||
@@ -7,3 +7,8 @@ type CreateTask struct {
|
||||
Req *base.Request `json:"req"`
|
||||
Opt *base.Options `json:"opt"`
|
||||
}
|
||||
|
||||
type CreateTaskBatch struct {
|
||||
Reqs []*base.Request `json:"reqs"`
|
||||
Opt *base.Options `json:"opt"`
|
||||
}
|
||||
+21
-8
@@ -19,31 +19,42 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
srv *http.Server
|
||||
srv *http.Server
|
||||
runningPort int
|
||||
|
||||
Downloader *download.Downloader
|
||||
)
|
||||
|
||||
func Start(startCfg *model.StartConfig) (int, error) {
|
||||
srv, listener, err := BuildServer(startCfg)
|
||||
func Start(startCfg *model.StartConfig) (port int, err error) {
|
||||
// avoid repeat start
|
||||
if srv != nil {
|
||||
return runningPort, nil
|
||||
}
|
||||
|
||||
var listener net.Listener
|
||||
srv, listener, err = BuildServer(startCfg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
port := 0
|
||||
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
|
||||
port = addr.Port
|
||||
runningPort = port
|
||||
}
|
||||
return port, nil
|
||||
return
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
defer func() {
|
||||
srv = nil
|
||||
}()
|
||||
|
||||
if srv != nil {
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
||||
@@ -88,8 +99,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
}
|
||||
|
||||
var r = mux.NewRouter()
|
||||
r.Methods(http.MethodGet).Path("/api/v1/info").HandlerFunc(Info)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
|
||||
@@ -141,7 +154,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
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))
|
||||
}
|
||||
}()
|
||||
|
||||
+30
-11
@@ -63,6 +63,18 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
matchKeys := []string{"version", "runtime", "os", "arch", "inDocker"}
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[map[string]any](http.MethodGet, "/api/v1/info", nil)
|
||||
for _, key := range matchKeys {
|
||||
if _, ok := resp[key]; !ok {
|
||||
t.Errorf("Info() missing key = %v", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
doTest(func() {
|
||||
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
|
||||
@@ -125,6 +137,21 @@ func TestCreateDirectTask(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||
doTest(func() {
|
||||
reqs := make([]*base.Request, 0)
|
||||
for i := 0; i < 5; i++ {
|
||||
reqs = append(reqs, createReq.Req)
|
||||
}
|
||||
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
||||
Reqs: reqs,
|
||||
})
|
||||
if len(taskIds) != len(reqs) {
|
||||
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
@@ -324,7 +351,7 @@ func TestGetTasks(t *testing.T) {
|
||||
|
||||
func TestGetAndPutConfig(t *testing.T) {
|
||||
doTest(func() {
|
||||
cfg := httpRequestCheckOk[*download.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
cfg.DownloadDir = "./download"
|
||||
cfg.Extra = map[string]any{
|
||||
"serverConfig": &Config{
|
||||
@@ -335,7 +362,7 @@ func TestGetAndPutConfig(t *testing.T) {
|
||||
}
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/config", cfg)
|
||||
|
||||
newCfg := httpRequestCheckOk[*download.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
newCfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
if !test.JsonEqual(cfg, newCfg) {
|
||||
t.Errorf("GetAndPutConfig() got = %v, want %v", test.ToJson(newCfg), test.ToJson(cfg))
|
||||
}
|
||||
@@ -556,15 +583,7 @@ func doTest(handler func()) {
|
||||
}()
|
||||
defer func() {
|
||||
Downloader.PauseAll()
|
||||
|
||||
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)
|
||||
}
|
||||
Downloader.DeleteByStatues(nil, true)
|
||||
os.RemoveAll(cfg.StorageDir)
|
||||
}()
|
||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||
|
||||
@@ -12,3 +12,17 @@ func MapToStruct(s any, v any) error {
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
func DeepClone[T any](v *T) *T {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var t T
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return &t
|
||||
}
|
||||
json.Unmarshal(b, &t)
|
||||
return &t
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeepClone(t *testing.T) {
|
||||
type user struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
|
||||
v int
|
||||
}
|
||||
|
||||
type args[T any] struct {
|
||||
v *T
|
||||
}
|
||||
type testCase[T any] struct {
|
||||
name string
|
||||
args args[T]
|
||||
want *T
|
||||
}
|
||||
tests := []testCase[user]{
|
||||
{
|
||||
name: "case 1",
|
||||
args: args[user]{
|
||||
v: &user{
|
||||
Name: "test",
|
||||
Age: 10,
|
||||
},
|
||||
},
|
||||
want: &user{
|
||||
Name: "test",
|
||||
Age: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "case 2",
|
||||
args: args[user]{
|
||||
v: &user{
|
||||
Name: "test",
|
||||
Age: 10,
|
||||
v: 1,
|
||||
},
|
||||
},
|
||||
want: &user{
|
||||
Name: "test",
|
||||
Age: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := DeepClone(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DeepClone() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+13
-7
@@ -2,23 +2,19 @@ package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const FileSchema = "FILE"
|
||||
|
||||
func ParseSchema(url string) string {
|
||||
index := strings.Index(url, ":")
|
||||
// if no schema or windows path like C:\a.txt, return FILE
|
||||
if index == -1 || index == 1 {
|
||||
return FileSchema
|
||||
return ""
|
||||
}
|
||||
schema := url[:index]
|
||||
if schema == "data" {
|
||||
schema, _ = ParseDataUri(url)
|
||||
}
|
||||
return strings.ToUpper(schema)
|
||||
}
|
||||
|
||||
@@ -31,7 +27,6 @@ func ParseDataUri(uri string) (string, []byte) {
|
||||
}
|
||||
mime := matches[1]
|
||||
base64Data := matches[2]
|
||||
// 解码Base64数据
|
||||
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
@@ -51,3 +46,14 @@ func BuildProxyUrl(scheme, host, usr, pwd string) *url.URL {
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyUrlToHandler gets the proxy handler from the proxy url.
|
||||
func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error) {
|
||||
if proxyUrl == nil {
|
||||
return nil
|
||||
}
|
||||
if proxyUrl.Scheme == "system" {
|
||||
return http.ProxyFromEnvironment
|
||||
}
|
||||
return http.ProxyURL(proxyUrl)
|
||||
}
|
||||
@@ -41,21 +41,21 @@ func TestParseSchema(t *testing.T) {
|
||||
args: args{
|
||||
url: "./url.go",
|
||||
},
|
||||
want: "FILE",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "data-uri",
|
||||
args: args{
|
||||
url: "data:application/x-bittorrent;base64,test",
|
||||
},
|
||||
want: "APPLICATION/X-BITTORRENT",
|
||||
want: "DATA",
|
||||
},
|
||||
{
|
||||
name: "windows-path",
|
||||
args: args{
|
||||
url: "D:\\bt.torrent",
|
||||
},
|
||||
want: "FILE",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
package="com.gopeed">
|
||||
|
||||
<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" />
|
||||
|
||||
<application
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Gopeed">
|
||||
android:label="Gopeed"
|
||||
android:extractNativeLibs="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
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 |
@@ -13,6 +13,7 @@
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
B44F54F47E581A39546303C9 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 029EF293EAA92EB3D18BBF19 /* libresolv.tbd */; };
|
||||
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D081C25B294826C0006EB10B /* libc++.tbd */; };
|
||||
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; };
|
||||
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -34,6 +35,7 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -63,6 +65,7 @@
|
||||
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */,
|
||||
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */,
|
||||
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */,
|
||||
B44F54F47E581A39546303C9 /* libresolv.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -130,6 +133,7 @@
|
||||
D0E623A729482D030001185E /* Libgopeed.xcframework */,
|
||||
D081C25B294826C0006EB10B /* libc++.tbd */,
|
||||
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */,
|
||||
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
||||
@@ -17,6 +17,7 @@ class DownloaderConfig {
|
||||
|
||||
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$DownloaderConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$DownloaderConfigToJson(this);
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ class ProtocolConfig {
|
||||
|
||||
factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
json == null ? ProtocolConfig() : _$ProtocolConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this);
|
||||
}
|
||||
|
||||
@@ -39,45 +41,65 @@ class HttpConfig {
|
||||
bool useServerCtime;
|
||||
|
||||
HttpConfig({
|
||||
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 = 16,
|
||||
this.userAgent = '',
|
||||
this.connections = 0,
|
||||
this.useServerCtime = false,
|
||||
});
|
||||
|
||||
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$HttpConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$HttpConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class BtConfig {
|
||||
int listenPort = 0;
|
||||
List<String> trackers = [];
|
||||
int listenPort;
|
||||
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) =>
|
||||
_$BtConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BtConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ExtraConfig {
|
||||
String themeMode = '';
|
||||
String locale = '';
|
||||
String themeMode;
|
||||
String locale;
|
||||
bool lastDeleteTaskKeep;
|
||||
bool defaultDirectDownload;
|
||||
|
||||
ExtraConfigBt bt = ExtraConfigBt();
|
||||
|
||||
ExtraConfig();
|
||||
ExtraConfig({
|
||||
this.themeMode = '',
|
||||
this.locale = '',
|
||||
this.lastDeleteTaskKeep = false,
|
||||
this.defaultDirectDownload = false,
|
||||
});
|
||||
|
||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
json == null ? ExtraConfig() : _$ExtraConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ProxyConfig {
|
||||
bool enable;
|
||||
bool system;
|
||||
String scheme;
|
||||
String host;
|
||||
String usr;
|
||||
@@ -85,6 +107,7 @@ class ProxyConfig {
|
||||
|
||||
ProxyConfig({
|
||||
this.enable = false,
|
||||
this.system = false,
|
||||
this.scheme = '',
|
||||
this.host = '',
|
||||
this.usr = '',
|
||||
@@ -110,5 +133,6 @@ class ExtraConfigBt {
|
||||
|
||||
factory ExtraConfigBt.fromJson(Map<String, dynamic> json) =>
|
||||
_$ExtraConfigBtFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ExtraConfigBtToJson(this);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ part of 'downloader_config.dart';
|
||||
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||
DownloaderConfig(
|
||||
downloadDir: json['downloadDir'] as String? ?? '',
|
||||
maxRunning: json['maxRunning'] as int? ?? 0,
|
||||
maxRunning: (json['maxRunning'] as num?)?.toInt() ?? 0,
|
||||
)
|
||||
..protocolConfig = ProtocolConfig.fromJson(
|
||||
json['protocolConfig'] as Map<String, dynamic>?)
|
||||
@@ -37,9 +37,8 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
|
||||
};
|
||||
|
||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||
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 int? ?? 16,
|
||||
userAgent: json['userAgent'] as String? ?? '',
|
||||
connections: (json['connections'] as num?)?.toInt() ?? 0,
|
||||
useServerCtime: json['useServerCtime'] as bool? ?? false,
|
||||
);
|
||||
|
||||
@@ -50,30 +49,44 @@ Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
|
||||
'useServerCtime': instance.useServerCtime,
|
||||
};
|
||||
|
||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
|
||||
..listenPort = json['listenPort'] as int
|
||||
..trackers =
|
||||
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
|
||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig(
|
||||
listenPort: (json['listenPort'] as num?)?.toInt() ?? 0,
|
||||
trackers: (json['trackers'] as List<dynamic>?)
|
||||
?.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>{
|
||||
'listenPort': instance.listenPort,
|
||||
'trackers': instance.trackers,
|
||||
'seedKeep': instance.seedKeep,
|
||||
'seedRatio': instance.seedRatio,
|
||||
'seedTime': instance.seedTime,
|
||||
};
|
||||
|
||||
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig()
|
||||
..themeMode = json['themeMode'] as String
|
||||
..locale = json['locale'] as String
|
||||
..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
themeMode: json['themeMode'] as String? ?? '',
|
||||
locale: json['locale'] as String? ?? '',
|
||||
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
||||
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
||||
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'themeMode': instance.themeMode,
|
||||
'locale': instance.locale,
|
||||
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||
'bt': instance.bt.toJson(),
|
||||
};
|
||||
|
||||
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
||||
enable: json['enable'] as bool? ?? false,
|
||||
system: json['system'] as bool? ?? false,
|
||||
scheme: json['scheme'] as String? ?? '',
|
||||
host: json['host'] as String? ?? '',
|
||||
usr: json['usr'] as String? ?? '',
|
||||
@@ -83,6 +96,7 @@ ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
||||
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'system': instance.system,
|
||||
'scheme': instance.scheme,
|
||||
'host': instance.host,
|
||||
'usr': instance.usr,
|
||||
|
||||
@@ -7,11 +7,13 @@ class Request {
|
||||
String url;
|
||||
Object? extra;
|
||||
Map<String, String>? labels = {};
|
||||
RequestProxy? proxy;
|
||||
|
||||
Request({
|
||||
required this.url,
|
||||
this.extra,
|
||||
this.labels,
|
||||
this.proxy,
|
||||
});
|
||||
|
||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -45,3 +47,24 @@ class ReqExtraBt {
|
||||
|
||||
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
|
||||
}
|
||||
|
||||
enum RequestProxyMode {
|
||||
follow,
|
||||
none,
|
||||
custom,
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class RequestProxy {
|
||||
RequestProxyMode mode = RequestProxyMode.follow;
|
||||
String scheme = 'http';
|
||||
String host = '';
|
||||
String usr = '';
|
||||
String pwd = '';
|
||||
|
||||
RequestProxy();
|
||||
|
||||
factory RequestProxy.fromJson(Map<String, dynamic> json) =>
|
||||
_$RequestProxyFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$RequestProxyToJson(this);
|
||||
}
|
||||
@@ -12,6 +12,9 @@ Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
||||
labels: (json['labels'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
proxy: json['proxy'] == null
|
||||
? null
|
||||
: RequestProxy.fromJson(json['proxy'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
@@ -27,6 +30,7 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
|
||||
writeNotNull('extra', instance.extra);
|
||||
writeNotNull('labels', instance.labels);
|
||||
writeNotNull('proxy', instance.proxy?.toJson());
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -50,3 +54,25 @@ Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
|
||||
<String, dynamic>{
|
||||
'trackers': instance.trackers,
|
||||
};
|
||||
|
||||
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy()
|
||||
..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode'])
|
||||
..scheme = json['scheme'] as String
|
||||
..host = json['host'] as String
|
||||
..usr = json['usr'] as String
|
||||
..pwd = json['pwd'] as String;
|
||||
|
||||
Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) =>
|
||||
<String, dynamic>{
|
||||
'mode': _$RequestProxyModeEnumMap[instance.mode]!,
|
||||
'scheme': instance.scheme,
|
||||
'host': instance.host,
|
||||
'usr': instance.usr,
|
||||
'pwd': instance.pwd,
|
||||
};
|
||||
|
||||
const _$RequestProxyModeEnumMap = {
|
||||
RequestProxyMode.follow: 'follow',
|
||||
RequestProxyMode.none: 'none',
|
||||
RequestProxyMode.custom: 'custom',
|
||||
};
|
||||
@@ -8,7 +8,7 @@ part of 'resource.dart';
|
||||
|
||||
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
|
||||
name: json['name'] as String? ?? "",
|
||||
size: json['size'] as int? ?? 0,
|
||||
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||
range: json['range'] as bool? ?? false,
|
||||
files: (json['files'] as List<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(
|
||||
path: json['path'] as String? ?? "",
|
||||
name: json['name'] as String,
|
||||
size: json['size'] as int? ?? 0,
|
||||
size: (json['size'] as num?)?.toInt() ?? 0,
|
||||
req: json['req'] == null
|
||||
? null
|
||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
|
||||
@@ -6,19 +6,26 @@ part 'task.g.dart';
|
||||
|
||||
enum Status { ready, running, pause, wait, error, done }
|
||||
|
||||
enum Protocol { http, bt }
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class Task {
|
||||
String id;
|
||||
String name;
|
||||
Protocol? protocol;
|
||||
Meta meta;
|
||||
Status status;
|
||||
bool uploading;
|
||||
Progress progress;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
|
||||
Task({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.meta,
|
||||
required this.status,
|
||||
required this.uploading,
|
||||
required this.progress,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@@ -34,11 +41,15 @@ class Progress {
|
||||
int used;
|
||||
int speed;
|
||||
int downloaded;
|
||||
int uploadSpeed;
|
||||
int uploaded;
|
||||
|
||||
Progress({
|
||||
required this.used,
|
||||
required this.speed,
|
||||
required this.downloaded,
|
||||
required this.uploadSpeed,
|
||||
required this.uploaded,
|
||||
});
|
||||
|
||||
factory Progress.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -8,21 +8,36 @@ part of 'task.dart';
|
||||
|
||||
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||
uploading: json['uploading'] as bool,
|
||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
)..protocol = $enumDecodeNullable(_$ProtocolEnumMap, json['protocol']);
|
||||
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'meta': instance.meta.toJson(),
|
||||
'status': _$StatusEnumMap[instance.status]!,
|
||||
'progress': instance.progress.toJson(),
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'updatedAt': instance.updatedAt.toIso8601String(),
|
||||
};
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
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 = {
|
||||
Status.ready: 'ready',
|
||||
@@ -33,14 +48,23 @@ const _$StatusEnumMap = {
|
||||
Status.done: 'done',
|
||||
};
|
||||
|
||||
const _$ProtocolEnumMap = {
|
||||
Protocol.http: 'http',
|
||||
Protocol.bt: 'bt',
|
||||
};
|
||||
|
||||
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
|
||||
used: json['used'] as int,
|
||||
speed: json['speed'] as int,
|
||||
downloaded: json['downloaded'] as int,
|
||||
used: (json['used'] as num).toInt(),
|
||||
speed: (json['speed'] as num).toInt(),
|
||||
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>{
|
||||
'used': instance.used,
|
||||
'speed': instance.speed,
|
||||
'downloaded': instance.downloaded,
|
||||
'uploadSpeed': instance.uploadSpeed,
|
||||
'uploaded': instance.uploaded,
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/database/entity.dart';
|
||||
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:uri_to_file/uri_to_file.dart';
|
||||
@@ -17,8 +17,11 @@ import 'package:window_manager/window_manager.dart';
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../core/common/start_config.dart';
|
||||
import '../../../../core/libgopeed_boot.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../database/entity.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/log_util.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
@@ -73,6 +76,7 @@ final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||
class AppController extends GetxController with WindowListener, TrayListener {
|
||||
static StartConfig? _defaultStartConfig;
|
||||
|
||||
final autoStartup = false.obs;
|
||||
final startConfig = StartConfig().obs;
|
||||
final runningPort = 0.obs;
|
||||
final downloaderConfig = DownloaderConfig().obs;
|
||||
@@ -98,12 +102,16 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
_initTrackerUpdate().onError((error, stackTrace) =>
|
||||
logger.w("initTrackerUpdate error", error, stackTrace));
|
||||
|
||||
_initLaunchAtStartup().onError((error, stackTrace) =>
|
||||
logger.w("initLaunchAtStartup error", error, stackTrace));
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_linkSubscription?.cancel();
|
||||
trayManager.removeListener(this);
|
||||
LibgopeedBoot.instance.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -114,6 +122,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
}
|
||||
|
||||
// According to the system_manager document, make sure to call setState once on the onWindowFocus event.
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconMouseDown() {
|
||||
windowManager.show();
|
||||
@@ -181,10 +195,20 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
} else if (Util.isMacos()) {
|
||||
await trayManager.setIcon('assets/tray_icon/icon_mac.png',
|
||||
isTemplate: true);
|
||||
} else if (Platform.environment.containsKey('FLATPAK_ID') ||
|
||||
Platform.environment.containsKey('SNAP')) {
|
||||
await trayManager.setIcon('com.gopeed.Gopeed');
|
||||
} else {
|
||||
await trayManager.setIcon('assets/tray_icon/icon.png');
|
||||
}
|
||||
final menu = Menu(items: [
|
||||
MenuItem(
|
||||
label: "show".tr,
|
||||
onClick: (menuItem) async => {
|
||||
await windowManager.show(),
|
||||
},
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: "create".tr,
|
||||
onClick: (menuItem) async => {
|
||||
@@ -192,7 +216,6 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE),
|
||||
},
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
label: "startAll".tr,
|
||||
onClick: (menuItem) async => {continueAllTasks()},
|
||||
@@ -222,7 +245,14 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
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()) {
|
||||
@@ -240,16 +270,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'gopeed_service',
|
||||
channelName: 'Gopeed Background Service',
|
||||
channelImportance: NotificationChannelImportance.LOW,
|
||||
showWhen: true,
|
||||
priority: NotificationPriority.LOW,
|
||||
iconData: const NotificationIconData(
|
||||
resType: ResourceType.mipmap,
|
||||
resPrefix: ResourcePrefix.ic,
|
||||
name: 'launcher',
|
||||
)),
|
||||
channelId: 'gopeed_service',
|
||||
channelName: 'Gopeed Background Service',
|
||||
channelImportance: NotificationChannelImportance.LOW,
|
||||
showWhen: true,
|
||||
priority: NotificationPriority.LOW,
|
||||
),
|
||||
iosNotificationOptions: const IOSNotificationOptions(
|
||||
showNotification: true,
|
||||
playSound: false,
|
||||
@@ -269,12 +295,19 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
FlutterForegroundTask.startService(
|
||||
notificationTitle: "serviceTitle".tr,
|
||||
notificationText: "serviceText".tr,
|
||||
notificationIcon: const NotificationIconData(
|
||||
resType: ResourceType.mipmap,
|
||||
resPrefix: ResourcePrefix.ic,
|
||||
name: 'launcher',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toCreate(Uri uri) async {
|
||||
final path = (uri.scheme == "magnet" || uri.scheme == "http" || uri.scheme == "https")
|
||||
final path = (uri.scheme == "magnet" ||
|
||||
uri.scheme == "http" ||
|
||||
uri.scheme == "https")
|
||||
? uri.toString()
|
||||
: (await toFile(uri.toString())).path;
|
||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
|
||||
@@ -305,22 +338,24 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
return _defaultStartConfig!;
|
||||
}
|
||||
|
||||
Future<void> loadStartConfig() async {
|
||||
Future<StartConfig> loadStartConfig() async {
|
||||
final defaultCfg = await _initDefaultStartConfig();
|
||||
final saveCfg = Database.instance.getStartConfig();
|
||||
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
|
||||
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
|
||||
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
|
||||
return startConfig.value;
|
||||
}
|
||||
|
||||
Future<void> loadDownloaderConfig() async {
|
||||
Future<DownloaderConfig> loadDownloaderConfig() async {
|
||||
try {
|
||||
downloaderConfig.value = await getConfig();
|
||||
} catch (e) {
|
||||
logger.w("load downloader config fail", e);
|
||||
logger.w("load downloader config fail", e, StackTrace.current);
|
||||
downloaderConfig.value = DownloaderConfig();
|
||||
}
|
||||
await _initDownloaderConfig();
|
||||
return downloaderConfig.value;
|
||||
}
|
||||
|
||||
Future<void> trackerUpdate() async {
|
||||
@@ -413,12 +448,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
if (Util.isDesktop()) {
|
||||
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
|
||||
} else if (Util.isAndroid()) {
|
||||
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
|
||||
if (downloadDir != null) {
|
||||
config.downloadDir = '$downloadDir/Gopeed';
|
||||
} else {
|
||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||
}
|
||||
config.downloadDir = (await getExternalStorageDirectory())?.path ??
|
||||
(await getApplicationDocumentsDirectory()).path;
|
||||
} else if (Util.isIOS()) {
|
||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||
} else {
|
||||
@@ -427,6 +458,17 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initLaunchAtStartup() async {
|
||||
if (!Util.isWindows() && !Util.isLinux()) {
|
||||
return;
|
||||
}
|
||||
launchAtStartup.setup(
|
||||
appName: packageInfo.appName,
|
||||
appPath: Platform.resolvedExecutable,
|
||||
args: ['--${Args.flagHidden}']);
|
||||
autoStartup.value = await launchAtStartup.isEnabled();
|
||||
}
|
||||
|
||||
Future<void> saveConfig() async {
|
||||
Database.instance.saveStartConfig(StartConfigEntity(
|
||||
network: startConfig.value.network,
|
||||
|
||||
@@ -3,6 +3,9 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/request.dart';
|
||||
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
|
||||
class CreateController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
@@ -13,6 +16,7 @@ class CreateController extends GetxController
|
||||
final isConfirming = false.obs;
|
||||
final showAdvanced = false.obs;
|
||||
final directDownload = false.obs;
|
||||
final proxyConfig = Rx<RequestProxy?>(null);
|
||||
late TabController advancedTabController;
|
||||
final oldUrl = "".obs;
|
||||
final fileDataUri = "".obs;
|
||||
@@ -21,6 +25,11 @@ class CreateController extends GetxController
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
advancedTabController = TabController(length: 2, vsync: this);
|
||||
directDownload.value = Get.find<AppController>()
|
||||
.downloaderConfig
|
||||
.value
|
||||
.extra
|
||||
.defaultDirectDownload;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/downloader_config.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
||||
|
||||
@@ -17,6 +18,7 @@ import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../../views/compact_checkbox.dart';
|
||||
import '../../../views/directory_selector.dart';
|
||||
import '../../../views/file_list_view.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
@@ -31,6 +33,10 @@ class CreateView extends GetView<CreateController> {
|
||||
final _connectionsController = TextEditingController();
|
||||
final _pathController = TextEditingController();
|
||||
final _confirmController = RoundedLoadingButtonController();
|
||||
final _proxyIpController = TextEditingController();
|
||||
final _proxyPortController = TextEditingController();
|
||||
final _proxyUsrController = TextEditingController();
|
||||
final _proxyPwdController = TextEditingController();
|
||||
final _httpUaController = TextEditingController();
|
||||
final _httpCookieController = TextEditingController();
|
||||
final _httpRefererController = TextEditingController();
|
||||
@@ -260,7 +266,164 @@ class CreateView extends GetView<CreateController> {
|
||||
() => Visibility(
|
||||
visible: controller.showAdvanced.value,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
controller: controller.advancedTabController,
|
||||
tabs: const [
|
||||
@@ -300,7 +463,7 @@ class CreateView extends GetView<CreateController> {
|
||||
controller: _btTrackerController,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Trakers',
|
||||
labelText: 'Trackers',
|
||||
hintText: 'addTrackerHit'.tr,
|
||||
)),
|
||||
],
|
||||
@@ -321,23 +484,13 @@ class CreateView extends GetView<CreateController> {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.directDownload.value =
|
||||
!controller.directDownload.value;
|
||||
},
|
||||
child: Row(children: [
|
||||
Obx(() => Checkbox(
|
||||
value:
|
||||
controller.directDownload.value,
|
||||
onChanged: (bool? value) {
|
||||
controller.directDownload.value =
|
||||
value ?? false;
|
||||
},
|
||||
)),
|
||||
Text('directDownload'.tr),
|
||||
]),
|
||||
),
|
||||
CompactCheckbox(
|
||||
label: 'directDownload'.tr,
|
||||
value: controller.directDownload.value,
|
||||
onChanged: (bool? value) {
|
||||
controller.directDownload.value =
|
||||
value ?? false;
|
||||
}),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.showAdvanced.value =
|
||||
@@ -405,7 +558,7 @@ class CreateView extends GetView<CreateController> {
|
||||
Util.isWeb() && controller.fileDataUri.isNotEmpty;
|
||||
final submitUrl = isWebFileChosen
|
||||
? controller.fileDataUri.value
|
||||
: _urlController.text;
|
||||
: _urlController.text.trim();
|
||||
|
||||
final urls = Util.textToLines(submitUrl);
|
||||
// Add url to the history
|
||||
@@ -415,7 +568,7 @@ class CreateView extends GetView<CreateController> {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
Check if is direct download, there has two ways to direct download
|
||||
1. Direct download option is checked
|
||||
2. Muli line urls
|
||||
@@ -425,18 +578,21 @@ class CreateView extends GetView<CreateController> {
|
||||
if (isDirect) {
|
||||
await Future.wait(urls.map((url) {
|
||||
return createTask(CreateTask(
|
||||
req: Request(url: url, extra: parseReqExtra(url)),
|
||||
req: Request(
|
||||
url: url, extra: parseReqExtra(url), proxy: parseProxy()),
|
||||
opt: Options(
|
||||
name: isMultiLine ? "" : _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles: [],
|
||||
extra: parseReqOptsExtra())));
|
||||
name: isMultiLine ? "" : _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles: [],
|
||||
extra: parseReqOptsExtra(),
|
||||
)));
|
||||
}));
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
} else {
|
||||
final rr = await resolve(Request(
|
||||
url: submitUrl,
|
||||
extra: parseReqExtra(_urlController.text),
|
||||
proxy: parseProxy(),
|
||||
));
|
||||
await _showResolveDialog(rr);
|
||||
}
|
||||
@@ -449,20 +605,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? reqExtra;
|
||||
if (controller.showAdvanced.value) {
|
||||
final u = Uri.parse(_urlController.text);
|
||||
if (u.scheme.startsWith("http")) {
|
||||
reqExtra = ReqExtraHttp()
|
||||
..header = {
|
||||
switch (controller.advancedTabController.index) {
|
||||
case 0:
|
||||
final header = {
|
||||
"User-Agent": _httpUaController.text,
|
||||
"Cookie": _httpCookieController.text,
|
||||
"Referer": _httpRefererController.text,
|
||||
};
|
||||
} else {
|
||||
reqExtra = ReqExtraBt()
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
header.removeWhere((key, value) => value.trim().isEmpty);
|
||||
if (header.isNotEmpty) {
|
||||
reqExtra = ReqExtraHttp()..header = header;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (_btTrackerController.text.trim().isNotEmpty) {
|
||||
reqExtra = ReqExtraBt()
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return reqExtra;
|
||||
@@ -545,7 +720,7 @@ class CreateView extends GetView<CreateController> {
|
||||
controller.selectedIndexes.map((index) {
|
||||
final file = rr.res.files[index];
|
||||
return createTask(CreateTask(
|
||||
req: file.req!,
|
||||
req: file.req!..proxy = parseProxy(),
|
||||
opt: Options(
|
||||
name: file.name,
|
||||
path: path.join(_pathController.text,
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/views/copy_button.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/log_util.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
import '../../../../util/util.dart';
|
||||
@@ -33,13 +38,22 @@ class SettingView extends GetView<SettingController> {
|
||||
final startCfg = appController.startConfig;
|
||||
|
||||
Timer? timer;
|
||||
debounceSave({bool needRestart = false}) {
|
||||
var completer = Completer<void>();
|
||||
Future<bool> debounceSave(
|
||||
{Future<String> Function()? check, bool needRestart = false}) {
|
||||
var completer = Completer<bool>();
|
||||
timer?.cancel();
|
||||
timer = Timer(const Duration(milliseconds: 1000), () {
|
||||
timer = Timer(const Duration(milliseconds: 1000), () async {
|
||||
if (check != null) {
|
||||
final checkResult = await check();
|
||||
if (checkResult.isNotEmpty) {
|
||||
showErrorMessage(checkResult);
|
||||
completer.complete(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
appController
|
||||
.saveConfig()
|
||||
.then(completer.complete)
|
||||
.then((_) => completer.complete(true))
|
||||
.onError(completer.completeError);
|
||||
if (needRestart) {
|
||||
showMessage('tip'.tr, 'effectAfterRestart'.tr);
|
||||
@@ -66,6 +80,7 @@ class SettingView extends GetView<SettingController> {
|
||||
return DirectorySelector(
|
||||
controller: downloadDirController,
|
||||
showLabel: false,
|
||||
showAndoirdToggle: true,
|
||||
);
|
||||
});
|
||||
final buildMaxRunning = _buildConfigItem(
|
||||
@@ -94,16 +109,82 @@ class SettingView extends GetView<SettingController> {
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
final buildDefaultDirectDownload =
|
||||
_buildConfigItem('defaultDirectDownload'.tr, () {
|
||||
return appController.downloaderConfig.value.extra.defaultDirectDownload
|
||||
? 'on'.tr
|
||||
: 'off'.tr;
|
||||
}, (Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value:
|
||||
appController.downloaderConfig.value.extra.defaultDirectDownload,
|
||||
onChanged: (bool value) async {
|
||||
appController.downloaderConfig.update((val) {
|
||||
val!.extra.defaultDirectDownload = value;
|
||||
});
|
||||
await debounceSave();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
buildBrowserExtension() {
|
||||
return ListTile(
|
||||
title: Text('browserExtension'.tr),
|
||||
subtitle: const OpenInNew(
|
||||
text: "Chrome",
|
||||
url:
|
||||
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
|
||||
));
|
||||
subtitle: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
OpenInNew(
|
||||
text: "Chrome",
|
||||
url:
|
||||
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
OpenInNew(
|
||||
text: "Edge",
|
||||
url:
|
||||
"https://microsoftedge.microsoft.com/addons/detail/dkajnckekendchdleoaenoophcobooce",
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
OpenInNew(
|
||||
text: "Firefox",
|
||||
url:
|
||||
"https://addons.mozilla.org/zh-CN/firefox/addon/gopeed-extension",
|
||||
),
|
||||
],
|
||||
).paddingOnly(top: 5));
|
||||
}
|
||||
|
||||
// Currently auto startup only support Windows and Linux
|
||||
final buildAutoStartup = !Util.isWindows() && !Util.isLinux()
|
||||
? () => null
|
||||
: _buildConfigItem('launchAtStartup'.tr, () {
|
||||
return appController.autoStartup.value ? 'on'.tr : 'off'.tr;
|
||||
}, (Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value: appController.autoStartup.value,
|
||||
onChanged: (bool value) async {
|
||||
try {
|
||||
if (value) {
|
||||
await launchAtStartup.enable();
|
||||
} else {
|
||||
await launchAtStartup.disable();
|
||||
}
|
||||
appController.autoStartup.value = value;
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
logger.e('launchAtStartup fail', e);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// http config items start
|
||||
final httpConfig = downloaderCfg.value.protocolConfig.http;
|
||||
final buildHttpUa =
|
||||
@@ -276,6 +357,69 @@ class SettingView extends GetView<SettingController> {
|
||||
},
|
||||
);
|
||||
});
|
||||
final buildBtSeedConfig = _buildConfigItem('seedConfig',
|
||||
() => '${'seedKeep'.tr}(${btConfig.seedKeep ? 'on'.tr : 'off'.tr})',
|
||||
(Key key) {
|
||||
final seedRatioController =
|
||||
TextEditingController(text: btConfig.seedRatio.toString());
|
||||
seedRatioController.addListener(() {
|
||||
if (seedRatioController.text.isNotEmpty) {
|
||||
btConfig.seedRatio = double.parse(seedRatioController.text);
|
||||
debounceSave();
|
||||
}
|
||||
});
|
||||
final seedTimeController =
|
||||
TextEditingController(text: (btConfig.seedTime ~/ 60).toString());
|
||||
seedTimeController.addListener(() {
|
||||
if (seedTimeController.text.isNotEmpty) {
|
||||
btConfig.seedTime = int.parse(seedTimeController.text) * 60;
|
||||
debounceSave();
|
||||
}
|
||||
});
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: btConfig.seedKeep,
|
||||
onChanged: (bool value) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.protocolConfig.bt.seedKeep = value;
|
||||
});
|
||||
debounceSave();
|
||||
},
|
||||
title: Text('seedKeep'.tr)),
|
||||
btConfig.seedKeep
|
||||
? null
|
||||
: TextField(
|
||||
controller: seedRatioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'seedRatio'.tr,
|
||||
),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d+\.?\d{0,2}')),
|
||||
],
|
||||
),
|
||||
btConfig.seedKeep
|
||||
? null
|
||||
: TextField(
|
||||
controller: seedTimeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'seedTime'.tr,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 0, max: 100000000),
|
||||
],
|
||||
),
|
||||
].where((e) => e != null).map((e) => e!).toList(),
|
||||
);
|
||||
});
|
||||
|
||||
// ui config items start
|
||||
final buildTheme = _buildConfigItem(
|
||||
@@ -408,29 +552,53 @@ class SettingView extends GetView<SettingController> {
|
||||
}
|
||||
|
||||
// advanced config proxy items start
|
||||
final proxy = downloaderCfg.value.proxy;
|
||||
final buildProxy = _buildConfigItem(
|
||||
'proxy',
|
||||
() => downloaderCfg.value.proxy.enable
|
||||
? '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}'
|
||||
: 'notSet'.tr,
|
||||
() {
|
||||
switch (proxy.proxyMode) {
|
||||
case ProxyModeEnum.noProxy:
|
||||
return 'noProxy'.tr;
|
||||
case ProxyModeEnum.systemProxy:
|
||||
return 'systemProxy'.tr;
|
||||
case ProxyModeEnum.customProxy:
|
||||
return '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}';
|
||||
}
|
||||
},
|
||||
(Key key) {
|
||||
final proxy = downloaderCfg.value.proxy;
|
||||
final mode = SizedBox(
|
||||
width: 150,
|
||||
child: DropdownButtonFormField<ProxyModeEnum>(
|
||||
value: proxy.proxyMode,
|
||||
onChanged: (value) async {
|
||||
if (value != null && value != proxy.proxyMode) {
|
||||
proxy.proxyMode = value;
|
||||
downloaderCfg.update((val) {
|
||||
val!.proxy = proxy;
|
||||
});
|
||||
|
||||
final switcher = Switch(
|
||||
value: proxy.enable,
|
||||
onChanged: (bool value) async {
|
||||
if (value != proxy.enable) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.proxy.enable = value;
|
||||
});
|
||||
|
||||
await debounceSave();
|
||||
}
|
||||
},
|
||||
await debounceSave();
|
||||
}
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem<ProxyModeEnum>(
|
||||
value: ProxyModeEnum.noProxy,
|
||||
child: Text('noProxy'.tr),
|
||||
),
|
||||
DropdownMenuItem<ProxyModeEnum>(
|
||||
value: ProxyModeEnum.systemProxy,
|
||||
child: Text('systemProxy'.tr),
|
||||
),
|
||||
DropdownMenuItem<ProxyModeEnum>(
|
||||
value: ProxyModeEnum.customProxy,
|
||||
child: Text('customProxy'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final scheme = SizedBox(
|
||||
width: 100,
|
||||
width: 150,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: proxy.scheme,
|
||||
onChanged: (value) async {
|
||||
@@ -478,7 +646,7 @@ class SettingView extends GetView<SettingController> {
|
||||
|
||||
ipController.addListener(updateAddress);
|
||||
portController.addListener(updateAddress);
|
||||
final server = [
|
||||
final server = Row(children: [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: ipController,
|
||||
@@ -503,12 +671,12 @@ class SettingView extends GetView<SettingController> {
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
]);
|
||||
|
||||
final usrController = TextEditingController(text: proxy.usr);
|
||||
final pwdController = TextEditingController(text: proxy.pwd);
|
||||
|
||||
final auth = [
|
||||
final auth = Row(children: [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: usrController,
|
||||
@@ -528,20 +696,21 @@ class SettingView extends GetView<SettingController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
]);
|
||||
|
||||
List<Widget> customView() {
|
||||
if (proxy.proxyMode != ProxyModeEnum.customProxy) {
|
||||
return [];
|
||||
}
|
||||
return [scheme, server, auth];
|
||||
}
|
||||
|
||||
return Form(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _addPadding([
|
||||
switcher,
|
||||
scheme,
|
||||
Row(
|
||||
children: server,
|
||||
),
|
||||
Row(
|
||||
children: auth,
|
||||
),
|
||||
mode,
|
||||
...customView(),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@@ -596,11 +765,41 @@ class SettingView extends GetView<SettingController> {
|
||||
final ipController = TextEditingController(text: ip);
|
||||
final portController = TextEditingController(text: port);
|
||||
updateAddress() async {
|
||||
if (ipController.text.isEmpty || portController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final newAddress = '${ipController.text}:${portController.text}';
|
||||
if (newAddress != startCfg.value.address) {
|
||||
startCfg.value.address = newAddress;
|
||||
|
||||
await debounceSave(needRestart: true);
|
||||
final saved = await debounceSave(
|
||||
check: () async {
|
||||
// Check if address already in use
|
||||
final configIp = ipController.text;
|
||||
final configPort = int.parse(portController.text);
|
||||
if (configPort == 0) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
final socket = await Socket.connect(configIp, configPort,
|
||||
timeout: const Duration(seconds: 3));
|
||||
socket.close();
|
||||
return 'portInUse'
|
||||
.trParams({'port': configPort.toString()});
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
needRestart: true);
|
||||
|
||||
// If save failed, restore the old address
|
||||
if (!saved) {
|
||||
final oldAddress =
|
||||
(await appController.loadStartConfig()).address;
|
||||
startCfg.update((val) async {
|
||||
val!.address = oldAddress;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,6 +865,32 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
});
|
||||
|
||||
// advanced config log items start
|
||||
buildLogsDir() {
|
||||
return ListTile(
|
||||
title: Text("logDirectory".tr),
|
||||
subtitle: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: TextEditingController(text: logsDir()),
|
||||
enabled: false,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
Util.isDesktop()
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () {
|
||||
launchUrl(Uri.file(logsDir()));
|
||||
},
|
||||
)
|
||||
: CopyButton(logsDir()),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -700,7 +925,9 @@ class SettingView extends GetView<SettingController> {
|
||||
children: _addDivider([
|
||||
buildDownloadDir(),
|
||||
buildMaxRunning(),
|
||||
buildBrowserExtension()
|
||||
buildDefaultDirectDownload(),
|
||||
buildBrowserExtension(),
|
||||
buildAutoStartup(),
|
||||
]),
|
||||
)),
|
||||
const Text('HTTP'),
|
||||
@@ -719,6 +946,7 @@ class SettingView extends GetView<SettingController> {
|
||||
buildBtListenPort(),
|
||||
buildBtTrackerSubscribeUrls(),
|
||||
buildBtTrackers(),
|
||||
buildBtSeedConfig(),
|
||||
]),
|
||||
)),
|
||||
Text('ui'.tr),
|
||||
@@ -775,6 +1003,13 @@ class SettingView extends GetView<SettingController> {
|
||||
: null,
|
||||
]),
|
||||
)),
|
||||
Text('developer'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildLogsDir(),
|
||||
]),
|
||||
)),
|
||||
]),
|
||||
))
|
||||
],
|
||||
@@ -857,3 +1092,37 @@ class SettingView extends GetView<SettingController> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ProxyModeEnum {
|
||||
noProxy,
|
||||
systemProxy,
|
||||
customProxy,
|
||||
}
|
||||
|
||||
extension ProxyMode on ProxyConfig {
|
||||
ProxyModeEnum get proxyMode {
|
||||
if (!enable) {
|
||||
return ProxyModeEnum.noProxy;
|
||||
}
|
||||
if (system) {
|
||||
return ProxyModeEnum.systemProxy;
|
||||
}
|
||||
return ProxyModeEnum.customProxy;
|
||||
}
|
||||
|
||||
set proxyMode(ProxyModeEnum value) {
|
||||
switch (value) {
|
||||
case ProxyModeEnum.noProxy:
|
||||
enable = false;
|
||||
break;
|
||||
case ProxyModeEnum.systemProxy:
|
||||
enable = true;
|
||||
system = true;
|
||||
break;
|
||||
case ProxyModeEnum.customProxy:
|
||||
enable = true;
|
||||
system = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/task.dart';
|
||||
|
||||
class TaskController extends GetxController {
|
||||
final tabIndex = 0.obs;
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final selectTask = Rx<Task?>(null);
|
||||
final copyUrlDone = false.obs;
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
return [
|
||||
IconButton(
|
||||
icon:
|
||||
const Icon(Icons.play_circle),
|
||||
const Icon(Icons.open_in_new),
|
||||
onPressed: () {
|
||||
launchUrl(Uri.parse(accessUrl),
|
||||
webOnlyWindowName:
|
||||
@@ -104,14 +104,9 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
}()
|
||||
: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_circle),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
onPressed: () async {
|
||||
print(filePath);
|
||||
try {
|
||||
await OpenFilex.open(filePath);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
await OpenFilex.open(filePath);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import 'package:flutter/material.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_downloaded_controller.dart';
|
||||
import '../controllers/task_downloading_controller.dart';
|
||||
@@ -12,9 +19,12 @@ class TaskView extends GetView<TaskController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectTask = controller.selectTask;
|
||||
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
key: controller.scaffoldKey,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(56),
|
||||
child: AppBar(
|
||||
@@ -54,7 +64,87 @@ class TaskView extends GetView<TaskController> {
|
||||
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,16 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import '../../api/api.dart';
|
||||
import '../../api/model/meta.dart';
|
||||
import '../../api/model/task.dart';
|
||||
import '../../util/file_explorer.dart';
|
||||
import '../../util/file_icon.dart';
|
||||
import '../../util/icons.dart';
|
||||
import '../../util/message.dart';
|
||||
import '../../util/util.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';
|
||||
|
||||
class BuildTaskListView extends GetView {
|
||||
@@ -57,18 +57,12 @@ class BuildTaskListView extends GetView {
|
||||
return task.status == Status.running;
|
||||
}
|
||||
|
||||
String buildExplorerUrl(Task task) {
|
||||
if (task.meta.res!.name.isEmpty) {
|
||||
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)));
|
||||
}
|
||||
bool isFolderTask() {
|
||||
return task.isFolder;
|
||||
}
|
||||
|
||||
Future<void> showDeleteDialog(String id) {
|
||||
final keep = true.obs;
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
final context = Get.context!;
|
||||
|
||||
@@ -78,11 +72,14 @@ class BuildTaskListView extends GetView {
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text('deleteTask'.tr),
|
||||
content: Obx(() => CheckboxListTile(
|
||||
value: keep.value,
|
||||
value: appController
|
||||
.downloaderConfig.value.extra.lastDeleteTaskKeep,
|
||||
title: Text('deleteTaskTip'.tr,
|
||||
style: context.textTheme.bodyLarge),
|
||||
onChanged: (v) {
|
||||
keep.value = v!;
|
||||
appController.downloaderConfig.update((val) {
|
||||
val!.extra.lastDeleteTaskKeep = v!;
|
||||
});
|
||||
})),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -96,7 +93,10 @@ class BuildTaskListView extends GetView {
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await deleteTask(id, !keep.value);
|
||||
final force = !appController
|
||||
.downloaderConfig.value.extra.lastDeleteTaskKeep;
|
||||
await appController.saveConfig();
|
||||
await deleteTask(id, force);
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
@@ -113,12 +113,7 @@ class BuildTaskListView extends GetView {
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () {
|
||||
if (Util.isDesktop()) {
|
||||
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
|
||||
} else {
|
||||
Get.rootDelegate
|
||||
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
|
||||
}
|
||||
task.explorer();
|
||||
},
|
||||
));
|
||||
} else {
|
||||
@@ -172,18 +167,26 @@ class BuildTaskListView extends GetView {
|
||||
(total > 0 ? " / ${Util.fmtByte(total)}" : "");
|
||||
}
|
||||
|
||||
final taskController = Get.find<TaskController>();
|
||||
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
onTap: () {
|
||||
taskController.scaffoldKey.currentState?.openEndDrawer();
|
||||
taskController.selectTask.value = task;
|
||||
},
|
||||
onDoubleTap: () {
|
||||
task.open();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(fileName(task.meta)),
|
||||
leading: (task.meta.res?.name.isNotEmpty ?? false
|
||||
title: Text(task.name),
|
||||
leading: isFolderTask()
|
||||
? const Icon(FaIcons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))]))),
|
||||
: Icon(FaIcons.allIcons[findIcon(task.name)])),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -214,29 +217,4 @@ class BuildTaskListView extends GetView {
|
||||
),
|
||||
)).padding(horizontal: 14, top: 8);
|
||||
}
|
||||
|
||||
String fileName(Meta meta) {
|
||||
if (meta.opts.name.isNotEmpty) {
|
||||
return meta.opts.name;
|
||||
}
|
||||
if (meta.res == null) {
|
||||
final u = Uri.parse(meta.req.url);
|
||||
if (u.scheme.startsWith("http")) {
|
||||
return u.path.isNotEmpty
|
||||
? u.path.substring(u.path.lastIndexOf("/") + 1)
|
||||
: u.host;
|
||||
} else {
|
||||
final params = u.queryParameters;
|
||||
if (params.containsKey("dn")) {
|
||||
return params["dn"]!;
|
||||
} else {
|
||||
return params["xt"]!.split(":").last;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meta.res!.name.isNotEmpty) {
|
||||
return meta.res!.name;
|
||||
}
|
||||
return meta.res!.files[0].name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CompactCheckbox extends StatefulWidget {
|
||||
final String label;
|
||||
final bool value;
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
final double? scale;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const CompactCheckbox({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
this.scale,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CompactCheckbox> createState() => _CompactCheckboxState();
|
||||
}
|
||||
|
||||
class _CompactCheckboxState extends State<CompactCheckbox> {
|
||||
late bool _value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_value = widget.value;
|
||||
}
|
||||
|
||||
valueChanged(bool? value) {
|
||||
setState(() {
|
||||
_value = value!;
|
||||
});
|
||||
widget.onChanged?.call(_value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final checkbox = Checkbox(
|
||||
value: _value,
|
||||
onChanged: valueChanged,
|
||||
);
|
||||
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
valueChanged(!_value);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
widget.scale == null
|
||||
? checkbox
|
||||
: Transform.scale(
|
||||
scale: widget.scale,
|
||||
child: checkbox,
|
||||
),
|
||||
Text(
|
||||
widget.label,
|
||||
style: widget.textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../util/message.dart';
|
||||
|
||||
class CopyButton extends StatefulWidget {
|
||||
final String? url;
|
||||
|
||||
const CopyButton(this.url, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CopyButton> createState() => _CopyButtonState();
|
||||
}
|
||||
|
||||
class _CopyButtonState extends State<CopyButton> {
|
||||
bool success = false;
|
||||
|
||||
copy() {
|
||||
final url = widget.url;
|
||||
if (url != null) {
|
||||
try {
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
setState(() {
|
||||
success = true;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
setState(() {
|
||||
success = false;
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: success ? const Icon(Icons.check_circle) : const Icon(Icons.copy),
|
||||
onPressed: copy,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,30 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
|
||||
import '../../util/mac_secure_util.dart';
|
||||
import '../../util/message.dart';
|
||||
import '../../util/util.dart';
|
||||
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
|
||||
class DirectorySelector extends StatefulWidget {
|
||||
final TextEditingController controller;
|
||||
final bool showLabel;
|
||||
final bool showAndoirdToggle;
|
||||
|
||||
const DirectorySelector(
|
||||
{Key? key, required this.controller, this.showLabel = true})
|
||||
{Key? key,
|
||||
required this.controller,
|
||||
this.showLabel = true,
|
||||
this.showAndoirdToggle = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -20,6 +34,82 @@ class DirectorySelector extends StatefulWidget {
|
||||
class _DirectorySelectorState extends State<DirectorySelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget? buildSelectWidget() {
|
||||
if (Util.isDesktop()) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
var dir = await FilePicker.platform.getDirectoryPath();
|
||||
if (dir != null) {
|
||||
widget.controller.text = dir;
|
||||
MacSecureUtil.saveBookmark(dir);
|
||||
}
|
||||
});
|
||||
}
|
||||
// After Android 11, access to external storage is increasingly restricted, so it no longer supports selecting the download directory. However, if you do not download in external storage, all downloaded files will be deleted after the application is uninstalled.
|
||||
// Fortunately, so far, most Android devices can still access the system download directory.
|
||||
// For the sake of user experience, it is decided to only support selecting the application's internal directory and the system download directory. Also, a test for file write permission is performed when selecting the system download directory. If it cannot be written, selection is not allowed.
|
||||
if (Util.isAndroid() && widget.showAndoirdToggle) {
|
||||
final isSwitchToDownloadDir =
|
||||
widget.controller.text.endsWith('/Gopeed');
|
||||
|
||||
return ToggleSwitch(
|
||||
initialLabelIndex: isSwitchToDownloadDir ? 1 : 0,
|
||||
totalSwitches: 2,
|
||||
icons: const [Icons.home, Icons.download],
|
||||
customWidths: const [50, 50],
|
||||
onToggle: (index) async {
|
||||
if (index == 0) {
|
||||
widget.controller.text =
|
||||
(await getExternalStorageDirectory())?.path ??
|
||||
(await getApplicationDocumentsDirectory()).path;
|
||||
} else {
|
||||
widget.controller.text =
|
||||
'${(await DownloadsPath.downloadsDirectory())!.path}/Gopeed';
|
||||
}
|
||||
},
|
||||
cancelToggle: (index) async {
|
||||
if (index == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final downloadDir =
|
||||
(await DownloadsPath.downloadsDirectory())?.path;
|
||||
if (downloadDir == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check and request external storage permission when sdk version < 30 (android 11)
|
||||
if ((await deviceInfo.androidInfo).version.sdkInt < 30) {
|
||||
var status = await Permission.storage.status;
|
||||
if (!status.isGranted) {
|
||||
status = await Permission.storage.request();
|
||||
if (!status.isGranted) {
|
||||
showErrorMessage('noStoragePermission'.tr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check write permission
|
||||
final fileRandomeName =
|
||||
"test_${DateTime.now().millisecondsSinceEpoch}.tmp";
|
||||
final testFile = File('$downloadDir/Gopeed/$fileRandomeName');
|
||||
try {
|
||||
await testFile.create(recursive: true);
|
||||
await testFile.writeAsString('test');
|
||||
await testFile.delete();
|
||||
return false;
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
).marginOnly(left: 10);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -35,17 +125,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
|
||||
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
|
||||
},
|
||||
)),
|
||||
!Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
var dir = await FilePicker.platform.getDirectoryPath();
|
||||
if (dir != null) {
|
||||
widget.controller.text = dir;
|
||||
MacSecureUtil.saveBookmark(dir);
|
||||
}
|
||||
})
|
||||
: null
|
||||
buildSelectWidget()
|
||||
].where((e) => e != null).map((e) => e!).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import '../../util/util.dart';
|
||||
import '../common/libgopeed_channel.dart';
|
||||
@@ -17,7 +16,19 @@ class LibgopeedBootNative implements LibgopeedBoot {
|
||||
late LibgopeedInterface _libgopeed;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -27,8 +38,7 @@ class LibgopeedBootNative implements LibgopeedBoot {
|
||||
cfg.storage = 'bolt';
|
||||
cfg.storageDir = Util.getStorageDir();
|
||||
cfg.refreshInterval = 0;
|
||||
var port =
|
||||
Util.isDesktop() ? await _ffiStart(cfg) : await _libgopeed.start(cfg);
|
||||
var port = await _libgopeed.start(cfg);
|
||||
return port;
|
||||
}
|
||||
|
||||
@@ -36,26 +46,4 @@ class LibgopeedBootNative implements LibgopeedBoot {
|
||||
Future<void> stop() async {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -54,15 +54,6 @@ class Database {
|
||||
_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
|
||||
void saveWindowState(WindowStateEntity entity) {
|
||||
final state = getWindowState();
|
||||
|
||||
@@ -17,6 +17,7 @@ const enUS = {
|
||||
'create': 'Create Task',
|
||||
'directDownload': 'Direct Download',
|
||||
'advancedOptions': 'Advanced Options',
|
||||
'followSettings': 'Follow Settings',
|
||||
'downloadLink': 'Download Link',
|
||||
'downloadLinkValid': 'Please enter the download link',
|
||||
'downloadLinkHit':
|
||||
@@ -35,6 +36,7 @@ const enUS = {
|
||||
'connections': 'Connections',
|
||||
'useServerCtime': 'Use server time for file creation',
|
||||
'maxRunning': 'Max Running Tasks',
|
||||
'defaultDirectDownload': 'Check direct download by default',
|
||||
'items': '@count items',
|
||||
'subscribeTracker': 'Subscribe Tracker',
|
||||
'subscribeFail':
|
||||
@@ -58,7 +60,11 @@ const enUS = {
|
||||
'apiToken': 'API Token',
|
||||
'notSet': 'NS',
|
||||
'set': 'SET',
|
||||
'portInUse': 'Port [@port] is in use, please change the port',
|
||||
'effectAfterRestart': 'Effect after restart',
|
||||
'developer': 'Developer',
|
||||
'logDirectory': 'Log Directory',
|
||||
'show': 'Show',
|
||||
'startAll': 'Start All',
|
||||
'pauseAll': 'Pause All',
|
||||
'deleteTask': 'Delete Task',
|
||||
@@ -82,6 +88,9 @@ const enUS = {
|
||||
'serviceText': 'Running',
|
||||
'network': 'Network',
|
||||
'proxy': 'Proxy',
|
||||
'noProxy': 'No Proxy',
|
||||
'systemProxy': 'System Proxy',
|
||||
'customProxy': 'Custom Proxy',
|
||||
'server': 'Server',
|
||||
'username': 'Username',
|
||||
'password': 'Password',
|
||||
@@ -89,5 +98,14 @@ const enUS = {
|
||||
'thanksDesc':
|
||||
'Thanks to all the contributors who have helped build and develop the Gopeed community!',
|
||||
'browserExtension': 'Browser Extension',
|
||||
'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',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
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',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
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',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
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',
|
||||
'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',
|
||||
'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',
|
||||
'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',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
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",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
const plPL = {
|
||||
'pl_PL': {
|
||||
'label': 'Polski',
|
||||
'error': 'Błąd',
|
||||
'tip': 'Tip',
|
||||
'confirm': 'Potwierdź',
|
||||
'cancel': 'Anuluj',
|
||||
'on': 'Włącz',
|
||||
'off': 'Wyłącz',
|
||||
'selectAll': 'Zaznacz wszystko',
|
||||
'task': 'Zadania',
|
||||
'downloading': 'Pobierane',
|
||||
'downloaded': 'Pobrane',
|
||||
'setting': 'Ustawienia',
|
||||
'donate': 'Wesprzyj',
|
||||
'exit': 'Wyjście',
|
||||
'create': 'Utwórz zadanie',
|
||||
'directDownload': 'Pobieranie bezpośrednie',
|
||||
'advancedOptions': 'Zaawansowane',
|
||||
'downloadLink': 'Pobierz z linku',
|
||||
'downloadLinkValid': 'Otwórz link pobierania',
|
||||
'downloadLinkHit':
|
||||
'Otwórz link pobierania, HTTP/HTTPS/MAGNET wspierane@append',
|
||||
'downloadLinkHitDesktop': ', lub przenieś bezpośrednio plik torrent',
|
||||
'download': 'Pobierz',
|
||||
'noFileSelected': 'Wybierz co najmniej jeden plik aby kontynuować.',
|
||||
'noStoragePermission': 'Przyznaj uprawnienia do zapisu',
|
||||
'selectFile': 'Wybierz plik',
|
||||
'rename': 'Zmień nazwę',
|
||||
'basic': 'Zwykły',
|
||||
'advanced': 'Zaawansowane',
|
||||
'general': 'Ogólne',
|
||||
'downloadDir': 'Folder Pobierania',
|
||||
'downloadDirValid': 'Wskaż folder pobierania',
|
||||
'connections': 'Połączenia',
|
||||
'useServerCtime': 'Użyj czasu serwera do utworzenia pliku',
|
||||
'maxRunning': 'Maksymalna liczba zadań',
|
||||
'items': '@count items',
|
||||
'subscribeTracker': 'Subskrybuj tracker',
|
||||
'subscribeFail': 'Błąd subskrypcji, sprawdź łączność z internetem',
|
||||
'update': 'Aktualizacja',
|
||||
'updateDaily': 'Aktualizuj codziennie',
|
||||
'lastUpdate': 'Ostatnia aktualizacja: @time',
|
||||
'addTracker': 'Dodaj tracker',
|
||||
'addTrackerHit': 'Wpisz adres trackera w jednej linii',
|
||||
'ui': 'Wygląd',
|
||||
'theme': 'Schemat kolorów',
|
||||
'themeSystem': 'Systemowy',
|
||||
'themeLight': 'Jasny',
|
||||
'themeDark': 'Ciemny',
|
||||
'locale': 'Język',
|
||||
'about': 'Info',
|
||||
'homepage': 'Strona WWW',
|
||||
'version': 'Wersja',
|
||||
'protocol': 'Protokół',
|
||||
'port': 'Port',
|
||||
'apiToken': 'API Token',
|
||||
'notSet': 'NS',
|
||||
'set': 'SET',
|
||||
'portInUse': 'Port [@port] w użyciu, proszę zmień port',
|
||||
'effectAfterRestart': 'Zmiany po restarcie',
|
||||
'show': 'Pokaż',
|
||||
'startAll': 'Zacznij wszystkie',
|
||||
'pauseAll': 'Zatrzymaj wszystkie',
|
||||
'deleteTask': 'Usuń zadanie',
|
||||
'deleteTaskTip': 'Zachowaj pobrane pliki',
|
||||
'delete': 'Usuń',
|
||||
'newVersionTitle': 'Sprawdź aktualizację @version',
|
||||
'newVersionUpdate': 'Aktualizuj teraz',
|
||||
'newVersionLater': 'Później',
|
||||
'extensions': 'Wtyczki',
|
||||
'extensionInstallUrl': 'Instaluj z url',
|
||||
'extensionInstallSuccess': 'Instalacja zakończona',
|
||||
'extensionUpdateSuccess': 'Aktualizacja zakończona',
|
||||
'extensionDelete': 'Usuń wtyczkę',
|
||||
'extensionAlreadyLatest': 'Wtyczka w aktualnej wersji',
|
||||
'extensionFind': 'Wyszukaj wtyczkę',
|
||||
'extensionDevelop': 'Stwórz wtyczkę',
|
||||
'history': 'Historia',
|
||||
'clearHistory': 'Wyczyść historię',
|
||||
'noHistoryFound': 'Brak historii',
|
||||
'serviceTitle': 'Usługa pobierania',
|
||||
'serviceText': 'Trwa',
|
||||
'network': 'Sieć',
|
||||
'proxy': 'Proxy',
|
||||
'noProxy': 'Bez Proxy',
|
||||
'systemProxy': 'Systemowe Proxy',
|
||||
'customProxy': 'Własne Proxy',
|
||||
'server': 'Serwer',
|
||||
'username': 'Użytkownik',
|
||||
'password': 'Hasło',
|
||||
'thanks': 'Dzięki',
|
||||
'thanksDesc':
|
||||
'Dziękuję wszystkim za pomoc przy powstawaniu aplikacji Gopeed!',
|
||||
'browserExtension': 'Wtyczka dla przeglądarki',
|
||||
'launchAtStartup': 'Uruchom na starcie',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
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': 'தொடக்கத்தின் போது இயங்கு',
|
||||
},
|
||||
};
|
||||
@@ -21,8 +21,7 @@ const trTR = {
|
||||
'downloadLinkValid': 'Lütfen indirme bağlantısını girin',
|
||||
'downloadLinkHit':
|
||||
'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',
|
||||
'noFileSelected': 'Devam etmek için lütfen en az bir dosya seçin.',
|
||||
'noStoragePermission': 'Depolama izni gerekli',
|
||||
@@ -44,8 +43,7 @@ const trTR = {
|
||||
'updateDaily': 'Günlük olarak güncelle',
|
||||
'lastUpdate': 'Son güncellenme tarihi: @time',
|
||||
'addTracker': 'İzleyici ekle',
|
||||
'addTrackerHit':
|
||||
'Lütfen her satıra bir tane olmak üzere izleyici sunucusu bağlantısını girin',
|
||||
'addTrackerHit': 'Lütfen her satıra bir tane olmak üzere izleyici sunucusu bağlantısını girin',
|
||||
'ui': 'Arayüz',
|
||||
'theme': 'Tema',
|
||||
'themeSystem': 'Sistem',
|
||||
@@ -58,9 +56,11 @@ const trTR = {
|
||||
'protocol': 'Protokol',
|
||||
'port': 'Bağlantı noktası',
|
||||
'apiToken': 'API anahtarı',
|
||||
'notSet': 'AYARLI',
|
||||
'set': 'AYARLI DEĞİL',
|
||||
'notSet': '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',
|
||||
'show': 'Göster',
|
||||
'startAll': 'Hepsini başlat',
|
||||
'pauseAll': 'Hepsini durdur',
|
||||
'deleteTask': 'Görevleri sil',
|
||||
@@ -84,6 +84,9 @@ const trTR = {
|
||||
'serviceText': 'Çalışıyor',
|
||||
'network': 'Ağ',
|
||||
'proxy': 'Vekil',
|
||||
'noProxy': 'Vekil yok',
|
||||
'systemProxy': 'Sistem vekili',
|
||||
'customProxy': 'Özel vekil',
|
||||
'server': 'Sunucu',
|
||||
'username': 'Kullanıcı adı',
|
||||
'password': 'Şifre',
|
||||
@@ -91,5 +94,6 @@ const trTR = {
|
||||
'thanksDesc':
|
||||
'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ı',
|
||||
'launchAtStartup': 'Başlangıçta başlat',
|
||||
},
|
||||
};
|
||||
@@ -17,6 +17,7 @@ const zhCN = {
|
||||
'create': '创建任务',
|
||||
'directDownload': '直接下载',
|
||||
'advancedOptions': '高级选项',
|
||||
'followSettings': '跟随设置',
|
||||
'downloadLink': '下载链接',
|
||||
'downloadLinkValid': '请输入下载链接',
|
||||
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
|
||||
@@ -34,6 +35,7 @@ const zhCN = {
|
||||
'connections': '连接数',
|
||||
'useServerCtime': '创建文件使用服务器时间',
|
||||
'maxRunning': '最大下载数',
|
||||
'defaultDirectDownload': '默认勾选直接下载',
|
||||
'items': '@count 项',
|
||||
'subscribeTracker': '订阅 Tracker',
|
||||
'subscribeFail': '订阅失败,请检查网络或稍后重试',
|
||||
@@ -56,7 +58,11 @@ const zhCN = {
|
||||
'apiToken': '接口令牌',
|
||||
'notSet': '未设置',
|
||||
'set': '已设置',
|
||||
'portInUse': '端口[@port]已被占用,请更换端口',
|
||||
'effectAfterRestart': '此配置项将在重启应用后生效',
|
||||
'developer': '开发者',
|
||||
'logDirectory': '日志目录',
|
||||
'show': '显示',
|
||||
'startAll': '全部开始',
|
||||
'pauseAll': '全部暂停',
|
||||
'deleteTask': '删除任务',
|
||||
@@ -80,11 +86,23 @@ const zhCN = {
|
||||
'serviceText': '运行中',
|
||||
'network': '网络',
|
||||
'proxy': '代理',
|
||||
'noProxy': '不使用代理',
|
||||
'systemProxy': '系统代理',
|
||||
'customProxy': '自定义代理',
|
||||
'server': '服务器',
|
||||
'username': '用户名',
|
||||
'password': '密码',
|
||||
'thanks': '鸣谢',
|
||||
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
|
||||
'browserExtension': '浏览器扩展',
|
||||
'launchAtStartup': '开机自动运行',
|
||||
'seedConfig': '做种设置',
|
||||
'seedKeep': '持续做种直到手动停止',
|
||||
'seedRatio': '做种分享率',
|
||||
'seedTime': '做种时间(分钟)',
|
||||
'taskDetail': '任务详情',
|
||||
'taskName': '任务名称',
|
||||
'taskUrl': '任务链接',
|
||||
'downloadPath': '下载路径',
|
||||
}
|
||||
};
|
||||
@@ -1,90 +1,108 @@
|
||||
const zhTW = {
|
||||
"zh_TW": {
|
||||
'label': '中文 (正體) ',
|
||||
'label': '中文 (正體)',
|
||||
'error': '錯誤',
|
||||
'tip': '提示',
|
||||
'confirm': '確定',
|
||||
'confirm': '確認',
|
||||
'cancel': '取消',
|
||||
'on': '開啟',
|
||||
'off': '關閉',
|
||||
'selectAll': '全選',
|
||||
'task': '任務',
|
||||
'downloading': '下載中',
|
||||
'downloaded': '已完成',
|
||||
'downloaded': '已下載',
|
||||
'setting': '設定',
|
||||
'donate': '鬥內',
|
||||
'exit': '離開',
|
||||
'create': '新增任務',
|
||||
'donate': '捐款',
|
||||
'exit': '退出',
|
||||
'create': '建立任務',
|
||||
'directDownload': '直接下載',
|
||||
'advancedOptions': '進階選項',
|
||||
'followSettings': '跟隨設定',
|
||||
'downloadLink': '下載連結',
|
||||
'downloadLinkValid': '請輸入下載連結',
|
||||
'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append',
|
||||
'downloadLinkHitDesktop': ',也可以通過拖拽Torrent檔至此以新增任務',
|
||||
'downloadLinkHitDesktop': ',或直接拖曳種子檔到此處',
|
||||
'download': '下載',
|
||||
'noFileSelected': '沒有選取的檔案',
|
||||
'noStoragePermission': '沒有存儲權',
|
||||
'noFileSelected': '請至少選擇一個檔案以繼續。',
|
||||
'noStoragePermission': '需要儲存空間權限',
|
||||
'selectFile': '選擇檔案',
|
||||
'rename': '重命名',
|
||||
'basic': '一般',
|
||||
'rename': '重新命名',
|
||||
'basic': '基本',
|
||||
'advanced': '進階',
|
||||
'general': '通用設定',
|
||||
'general': '一般',
|
||||
'downloadDir': '下載目錄',
|
||||
'downloadDirValid': '請選擇下載目錄',
|
||||
'connections': '同時連接數',
|
||||
'connections': '連接數',
|
||||
'useServerCtime': '使用伺服器時間作為檔案建立時間',
|
||||
'maxRunning': '最大同時下載數',
|
||||
'items': '@count 項',
|
||||
'subscribeTracker': '已訂閱的追蹤者列表',
|
||||
'subscribeFail': '訂閱失敗',
|
||||
'maxRunning': '最大執行任務數',
|
||||
'defaultDirectDownload': '預設勾選直接下載',
|
||||
'items': '@count 個項目',
|
||||
'subscribeTracker': '訂閱 Tracker',
|
||||
'subscribeFail': '訂閱失敗,請檢查網路或稍後再試',
|
||||
'update': '更新',
|
||||
'updateDaily': '每天自動更新',
|
||||
'lastUpdate': '上次更新時間: @time',
|
||||
'addTracker': '增加新的追蹤者',
|
||||
'addTrackerHit': '請輸入追蹤者伺服器地址,每行一條',
|
||||
'updateDaily': '每日更新',
|
||||
'lastUpdate': '最後更新時間:@time',
|
||||
'addTracker': '新增 Tracker',
|
||||
'addTrackerHit': '請輸入 Tracker 伺服器 URL,每行一個',
|
||||
'ui': '介面',
|
||||
'theme': '主題',
|
||||
'themeSystem': '跟隨系統',
|
||||
'themeLight': '亮色主題',
|
||||
'themeDark': '深色主題',
|
||||
'themeSystem': '系統',
|
||||
'themeLight': '淺色',
|
||||
'themeDark': '深色',
|
||||
'locale': '語言',
|
||||
'about': '關於',
|
||||
'homepage': '訪問主頁',
|
||||
'version': '版本資訊',
|
||||
'protocol': '通信協定',
|
||||
'port': '埠',
|
||||
'apiToken': 'API令牌',
|
||||
'homepage': '首頁',
|
||||
'version': '版本',
|
||||
'protocol': '協議',
|
||||
'port': '連接埠',
|
||||
'apiToken': 'API Token',
|
||||
'notSet': '未設定',
|
||||
'set': '已設定',
|
||||
'effectAfterRestart': '重啟後生效',
|
||||
'portInUse': '連接埠 [@port] 已被使用,請更改連接埠',
|
||||
'effectAfterRestart': '重新啟動後生效',
|
||||
'developer': '開發者',
|
||||
'logDirectory': '日誌目錄',
|
||||
'show': '顯示',
|
||||
'startAll': '全部開始',
|
||||
'pauseAll': '全部暫停',
|
||||
'deleteTask': '刪除任務',
|
||||
'deleteTaskTip': '保留已經下載的檔案',
|
||||
'deleteTaskTip': '保留已下載的檔案',
|
||||
'delete': '刪除',
|
||||
'newVersionTitle': '新版本 @version 已發佈',
|
||||
'newVersionUpdate': '下載',
|
||||
'newVersionLater': '取消',
|
||||
'extensions': '擴充程式',
|
||||
'extensionInstallUrl': '安裝連結',
|
||||
'newVersionTitle': '發現新版本 @version',
|
||||
'newVersionUpdate': '立即更新',
|
||||
'newVersionLater': '稍後',
|
||||
'extensions': '擴充功能',
|
||||
'extensionInstallUrl': '安裝 URL',
|
||||
'extensionInstallSuccess': '安裝成功',
|
||||
'extensionUpdateSuccess': '更新完成',
|
||||
'extensionDelete': '解除安裝',
|
||||
'extensionUpdateSuccess': '更新成功',
|
||||
'extensionDelete': '刪除擴充功能',
|
||||
'extensionAlreadyLatest': '已是最新版本',
|
||||
'extensionFind': '取得擴充程式',
|
||||
'extensionDevelop': '開發擴充程式',
|
||||
'extensionFind': '尋找擴充功能',
|
||||
'extensionDevelop': '開發擴充功能',
|
||||
'history': '歷史記錄',
|
||||
'clearHistory': '清空記錄',
|
||||
'noHistoryFound': '還沒有記錄',
|
||||
'clearHistory': '清除歷史記錄',
|
||||
'noHistoryFound': '未找到歷史記錄',
|
||||
'serviceTitle': '下載服務',
|
||||
'serviceText': '執行中',
|
||||
'serviceText': '運行中',
|
||||
'network': '網路',
|
||||
'proxy': '代理',
|
||||
'proxy': 'Proxy',
|
||||
'noProxy': '沒有 Proxy',
|
||||
'systemProxy': '系統 Proxy',
|
||||
'customProxy': '自訂 Proxy',
|
||||
'server': '伺服器',
|
||||
'username': '名稱',
|
||||
'username': '使用者名稱',
|
||||
'password': '密碼',
|
||||
'thanks': '感謝',
|
||||
'thanksDesc': '感謝所有為 Gopeed 社區建設添磚加瓦的貢獻者們!',
|
||||
'browserExtension': '瀏覽器擴充程式',
|
||||
'thanksDesc': '感謝所有幫助建立和發展 Gopeed 社群的貢獻者!',
|
||||
'browserExtension': '瀏覽器擴充功能',
|
||||
'launchAtStartup': '開機自動執行',
|
||||
'seedConfig': '做種設定',
|
||||
'seedKeep': '持續做種直到手動停止',
|
||||
'seedRatio': '做種分享率',
|
||||
'seedTime': '做種時間(分鐘)',
|
||||
'taskDetail': '任務詳情',
|
||||
'taskName': '任務名稱',
|
||||
'taskUrl': '任務連結',
|
||||
'downloadPath': '下載路徑',
|
||||
}
|
||||
};
|
||||
@@ -1,13 +1,19 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/i18n/langs/fa_ir.dart';
|
||||
import 'package:gopeed/i18n/langs/ja_jp.dart';
|
||||
import 'package:gopeed/i18n/langs/zh_tw.dart';
|
||||
|
||||
import 'langs/en_us.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/pl_pl.dart';
|
||||
import 'langs/ru_ru.dart';
|
||||
import 'langs/zh_cn.dart';
|
||||
import 'langs/vi_vn.dart';
|
||||
import 'langs/ta_ta.dart';
|
||||
import 'langs/tr_tr.dart';
|
||||
import 'langs/vi_vn.dart';
|
||||
import 'langs/zh_cn.dart';
|
||||
import 'langs/zh_tw.dart';
|
||||
import 'langs/es_es.dart';
|
||||
|
||||
final messages = _Messages();
|
||||
|
||||
@@ -22,6 +28,12 @@ class _Messages extends Translations {
|
||||
...faIR,
|
||||
...jaJP,
|
||||
...viVN,
|
||||
...taTA,
|
||||
...trTR,
|
||||
...plPL,
|
||||
...itIT,
|
||||
...idID,
|
||||
...frFR,
|
||||
...esES,
|
||||
};
|
||||
}
|
||||
+30
-30
@@ -1,6 +1,6 @@
|
||||
import 'package:args/args.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'app/modules/app/controllers/app_controller.dart';
|
||||
import 'app/modules/app/views/app_view.dart';
|
||||
import 'core/libgopeed_boot.dart';
|
||||
import 'database/database.dart';
|
||||
import 'database/entity.dart';
|
||||
import 'i18n/message.dart';
|
||||
import 'util/locale_manager.dart';
|
||||
import 'util/log_util.dart';
|
||||
@@ -17,14 +16,28 @@ import 'util/mac_secure_util.dart';
|
||||
import 'util/package_info.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() async {
|
||||
await init();
|
||||
class Args {
|
||||
static const flagHidden = "hidden";
|
||||
|
||||
bool hidden = false;
|
||||
|
||||
Args.parse(List<String> args) {
|
||||
final parser = ArgParser();
|
||||
parser.addFlag(flagHidden);
|
||||
final results = parser.parse(args);
|
||||
hidden = results.flag(flagHidden);
|
||||
}
|
||||
}
|
||||
|
||||
void main(List<String> arguments) async {
|
||||
final args = Args.parse(arguments);
|
||||
await init(args);
|
||||
onStart();
|
||||
|
||||
runApp(const AppView());
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
Future<void> init(Args args) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Util.initStorageDir();
|
||||
await Database.instance.init();
|
||||
@@ -37,8 +50,10 @@ Future<void> init() async {
|
||||
skipTaskbar: false,
|
||||
);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
if (!args.hidden) {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
}
|
||||
await windowManager.setPreventClose(true);
|
||||
// windows_manager has a bug where when window to be maximized, it will be unmaximized immediately, so can't implement this feature currently.
|
||||
// https://github.com/leanflutter/window_manager/issues/412
|
||||
@@ -50,27 +65,18 @@ Future<void> init() async {
|
||||
|
||||
initLogger();
|
||||
|
||||
try {
|
||||
await initPackageInfo();
|
||||
} catch (e) {
|
||||
logger.e("init package info fail", e);
|
||||
}
|
||||
|
||||
final controller = Get.put(AppController());
|
||||
try {
|
||||
await controller.loadStartConfig();
|
||||
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.
|
||||
final isRunning =
|
||||
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);
|
||||
}
|
||||
controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg);
|
||||
api.init(startCfg.network, controller.runningAddress(), startCfg.apiToken);
|
||||
} catch (e) {
|
||||
logger.e("libgopeed init fail", e);
|
||||
}
|
||||
@@ -81,12 +87,6 @@ Future<void> init() async {
|
||||
} catch (e) {
|
||||
logger.e("load config fail", e);
|
||||
}
|
||||
|
||||
try {
|
||||
await initPackageInfo();
|
||||
} catch (e) {
|
||||
logger.e("init package info fail", e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onStart() async {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class FileExplorer {
|
||||
static Future<void> openAndSelectFile(String filePath) async {
|
||||
if (await FileSystemEntity.isFile(filePath)) {
|
||||
@@ -10,17 +12,7 @@ class FileExplorer {
|
||||
}
|
||||
|
||||
static Future<void> _openDirectory(String directoryPath) async {
|
||||
if (Platform.isWindows) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
await launchUrlString("file://$directoryPath");
|
||||
}
|
||||
|
||||
static Future<void> _openFile(String filePath) async {
|
||||
|
||||
@@ -15,11 +15,15 @@ initLogger() {
|
||||
);
|
||||
}
|
||||
|
||||
String logsDir() {
|
||||
return path.join(Util.getStorageDir(), 'logs');
|
||||
}
|
||||
|
||||
_buildOutput() {
|
||||
// if is debug mode, don't log to file
|
||||
if (!kDebugMode && Util.isDesktop()) {
|
||||
final logDirPath = path.join(Util.getStorageDir(), 'logs');
|
||||
var logDir = Directory(logDirPath);
|
||||
final logDirPath = logsDir();
|
||||
var logDir = Directory(logsDir());
|
||||
if (!logDir.existsSync()) {
|
||||
logDir.createSync();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class Util {
|
||||
static Future<void> initStorageDir() async {
|
||||
var storageDir = "";
|
||||
if (Util.isWindows()) {
|
||||
storageDir = "./";
|
||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
||||
} else if (!Util.isWeb()) {
|
||||
if (Util.isLinux()) {
|
||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
||||
|
||||
@@ -48,7 +48,8 @@ static void my_application_activate(GApplication* application) {
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
//gtk_widget_show(GTK_WIDGET(window));
|
||||
gtk_widget_realize(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user