Compare commits

..
Author SHA1 Message Date
liwei c7fefd2b9b fix: test 2024-05-01 18:39:24 +08:00
liwei b8e36b483e fix: test 2024-05-01 18:38:13 +08:00
liwei 91ac841b9d setting ui 2024-05-01 18:35:32 +08:00
liwei 5134446c3f fix: test 2024-05-01 18:35:32 +08:00
liwei 82b6ebd6f1 feat: support proxy from system env 2024-05-01 18:35:32 +08:00
liwei f8ea009fe9 temp 2024-05-01 18:35:32 +08:00
117 changed files with 1145 additions and 5549 deletions

No files matched your search

-16
View File
@@ -1,16 +0,0 @@
.git
*.data
*.log
node_modules
**/node_modules
Dockerfile
.dockerignore
.github
_docs
_examples
bin
ui
-1
View File
@@ -1 +0,0 @@
* @monkeyWie
-3
View File
@@ -11,9 +11,6 @@ categories:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🔧 Performance Improvements'
labels:
- 'perf'
- title: '🧪 Tests'
label: 'test'
- title: '🧰 Maintenance'
+16 -56
View File
@@ -13,7 +13,7 @@ on:
default: "false"
env:
GO_VERSION: "1.22"
GO_VERSION: "1.21"
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: macos-latest
runs-on: ubuntu-latest
needs: [ get-release ]
steps:
- uses: actions/checkout@v3
@@ -147,14 +147,16 @@ jobs:
env:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
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
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
- 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-13
runs-on: macos-latest
needs: [ get-release, build-macos-arm64-lib ]
steps:
- uses: actions/checkout@v3
@@ -169,7 +171,7 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install appdmg
run: |
python3 -m pip install setuptools --break-system-packages
python3 -m pip install setuptools
npm install -g appdmg
- uses: actions/download-artifact@v3
with:
@@ -258,6 +260,9 @@ 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:
@@ -295,55 +300,20 @@ jobs:
apps:
gopeed:
command: bin/gopeed
command: 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: nil
plugin: flutter
stage-packages:
- libgtk-3-dev
- libappindicator3-dev
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/
flutter-target: lib/main.dart
EOF
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
@@ -380,7 +350,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 21 -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 19 -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
@@ -411,7 +381,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/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
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
cd ui/flutter
flutter build ios --no-codesign
mkdir Payload
@@ -483,9 +453,6 @@ 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
@@ -496,19 +463,11 @@ 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 }}
@@ -518,6 +477,7 @@ jobs:
linux/amd64
linux/arm64
linux/arm/v7
linux/arm64/v8
tags: |
liwei2633/gopeed:latest
liwei2633/gopeed:v${{ env.VERSION }}
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile"
env:
GO_VERSION: "1.22"
GO_VERSION: "1.19"
permissions:
contents: write
+2 -3
View File
@@ -27,7 +27,7 @@ on:
- "go.sum"
env:
GO_VERSION: "1.22"
GO_VERSION: "1.21"
FLUTTER_VERSION: "3.16.8"
jobs:
@@ -54,10 +54,9 @@ 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@v4
- uses: codecov/codecov-action@v3
with:
files: ./coverage.txt
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
build:
name: Build check
runs-on: ubuntu-latest
-1
View File
@@ -21,7 +21,6 @@ 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
+9 -1
View File
@@ -1,8 +1,16 @@
FROM golang:1.22.6 AS go
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
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" \
+1 -3
View File
@@ -5,8 +5,6 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<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>
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -174,7 +172,7 @@ command:
- android
```bash
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
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
cd ui/flutter
flutter build apk
```
+2 -4
View File
@@ -5,8 +5,6 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<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>
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -102,7 +100,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
### 環境
1. Go 言語 1.22+
1. Go 言語 1.21+
2. Flutter 3.16+
### クローン
@@ -162,7 +160,7 @@ gomobile init
- android
```bash
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
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
cd ui/flutter
flutter build apk
```
+2 -4
View File
@@ -5,8 +5,6 @@
[![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<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>
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -110,7 +108,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.22+
1. Golang 1.21+
2. Flutter 3.16+
### Clone
@@ -170,7 +168,7 @@ command:
- android
```bash
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
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
cd ui/flutter
flutter build apk
```
+2 -4
View File
@@ -5,8 +5,6 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<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>
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -114,7 +112,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
### 环境要求
1. Golang 1.22+
1. Golang 1.21+
2. Flutter 3.16+
### 克隆项目
@@ -174,7 +172,7 @@ gomobile init
- android
```bash
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
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
cd ui/flutter
flutter build apk
```
+2 -4
View File
@@ -5,8 +5,6 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<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>
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -110,7 +108,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
### 開發環境
1. Golang 1.22+
1. Golang 1.21+
2. Flutter 3.16+
### 克隆項目
@@ -170,7 +168,7 @@ gomobile init
- android
```bash
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
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
cd ui/flutter
flutter build apk
```
+11 -36
View File
@@ -8,9 +8,7 @@ import (
"github.com/GopeedLab/gopeed/pkg/rest/model"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
)
//go:embed banner.txt
@@ -27,46 +25,23 @@ 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
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)
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 != "" {
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)
}()
}
+2 -6
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"flag"
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"os"
"path/filepath"
)
@@ -16,8 +15,6 @@ 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
}
@@ -28,7 +25,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, it must be configured when using HTTP API in the case of enabling basic authentication")
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse()
@@ -60,9 +57,8 @@ func loadConfig(path string) *args {
var args args
if !filepath.IsAbs(path) {
dir, err := os.Getwd()
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
fmt.Println("config dir get failed, reason:" + err.Error())
return &args
}
path = filepath.Join(dir, path)
-1
View File
@@ -48,7 +48,6 @@ func main() {
Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken,
DownloadConfig: args.DownloadConfig,
ProductionMode: true,
WebEnable: true,
WebFS: sub,
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
chown -R ${PUID}:${PGID} /app
+51 -61
View File
@@ -1,58 +1,54 @@
module github.com/GopeedLab/gopeed
go 1.22
go 1.21
require (
github.com/anacrolix/missinggo/v2 v2.7.3
github.com/anacrolix/torrent v1.56.1
github.com/anacrolix/torrent v1.53.3
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
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
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
go.etcd.io/bbolt v1.3.8
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/sync v0.6.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.9.4 // indirect
github.com/RoaringBitmap/roaring v1.7.0 // 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.5.1 // indirect
github.com/anacrolix/dht/v2 v2.21.1 // indirect
github.com/anacrolix/chansync v0.4.0 // indirect
github.com/anacrolix/dht/v2 v2.21.0 // indirect
github.com/anacrolix/envpprof v1.3.0 // indirect
github.com/anacrolix/generics v0.0.2 // indirect
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
github.com/anacrolix/go-libutp v1.3.1 // indirect
github.com/anacrolix/log v0.15.2 // indirect
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 // 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.4 // indirect
github.com/anacrolix/upnp v0.1.3 // 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.11.4 // indirect
github.com/dlclark/regexp2 v1.10.0 // 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
@@ -60,69 +56,63 @@ 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.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-sourcemap/sourcemap v2.1.3+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-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/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/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-ieproxy v0.0.11 // 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/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/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/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.12 // indirect
github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // 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/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/stun v0.6.1 // 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/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/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/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/stretchr/testify v1.8.4 // 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
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
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
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.23.0 // indirect
golang.org/x/tools v0.17.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
modernc.org/libc v1.55.6 // indirect
modernc.org/libc v1.40.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.31.1 // indirect
zombiezen.com/go/sqlite v1.3.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
zombiezen.com/go/sqlite v1.1.0 // indirect
)
+162 -155
View File
@@ -5,20 +5,17 @@ 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.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
github.com/RoaringBitmap/roaring v1.7.0/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=
@@ -26,35 +23,33 @@ 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.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/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/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.2 h1:UbtD+KntUGxeGYMC4RwhsETieL9ixGdSptJQRhdy7No=
github.com/anacrolix/generics v0.0.2/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/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.15.2 h1:LTSf5Wm6Q4GNWPFMBP7NPYV6UBVZzZLKckL+/Lj72Oo=
github.com/anacrolix/log v0.15.2/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA=
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/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=
@@ -84,14 +79,13 @@ 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.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/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/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=
@@ -111,9 +105,10 @@ 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=
@@ -122,13 +117,18 @@ 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.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
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/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
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/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/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,7 +139,6 @@ 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=
@@ -147,10 +146,9 @@ 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=
@@ -162,25 +160,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.3 h1:PmHXJjjxHodMoFWs3dEcJ1I1m9a+J0TCMNiHjGbXy1o=
github.com/go-llsqlite/crawshaw v0.5.3/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
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-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.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/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.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+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=
@@ -198,6 +196,9 @@ 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=
@@ -208,15 +209,16 @@ 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-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
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/uuid v1.3.1/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/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.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=
@@ -227,18 +229,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.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
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.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/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/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=
@@ -249,8 +251,6 @@ 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,84 @@ 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-ieproxy v0.0.11 h1:MQ/5BuGSgDAHZOJe6YY80IF2UVCfGkwfo6AeD7HtHYo=
github.com/mattn/go-ieproxy v0.0.11/go.mod h1:/NsJd+kxZBmjMc5hrJCKMbP57B84rvq9BiDRbtO9AS0=
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.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
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/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
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/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/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.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/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/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.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
github.com/pion/rtcp v1.2.14/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/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
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/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/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
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/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/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 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
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.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/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -385,6 +387,7 @@ 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=
@@ -396,27 +399,23 @@ 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=
@@ -426,53 +425,54 @@ 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.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
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.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=
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=
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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
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/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.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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/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=
@@ -484,20 +484,26 @@ 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=
@@ -506,11 +512,12 @@ 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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/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=
@@ -520,19 +527,27 @@ 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=
@@ -540,35 +555,40 @@ 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.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/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.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=
@@ -578,11 +598,12 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-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.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
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=
@@ -601,6 +622,9 @@ 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=
@@ -616,6 +640,7 @@ 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=
@@ -623,31 +648,13 @@ 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=
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/libc v1.40.6 h1:141JHq3SjhOOCjECBgD4K8VgTFOy19CnHwroC08DAig=
modernc.org/libc v1.40.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
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.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=
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=
+5 -6
View File
@@ -2,15 +2,13 @@ package controller
import (
"github.com/GopeedLab/gopeed/pkg/base"
"net/http"
"net/url"
"os"
"path/filepath"
)
type Controller struct {
GetConfig func(v any)
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
GetConfig func(v any) bool
ProxyConfig *base.DownloaderProxyConfig
FileController
//ContextDialer() (proxy.Dialer, error)
}
@@ -24,9 +22,10 @@ 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
},
}
}
+8 -52
View File
@@ -4,12 +4,14 @@ import (
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/pkg/base"
"path"
"strings"
)
// Fetcher defines the interface for a download protocol.
// Each download task will have a corresponding Fetcher instance for the management of the download task
// One fetcher for each 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
@@ -29,12 +31,6 @@ 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"`
@@ -72,57 +68,17 @@ func (m *FetcherMeta) RootDirPath() 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
// FetcherBuilder defines the interface for a fetcher builder.
type FetcherBuilder interface {
// Schemes returns the schemes supported by the fetcher.
Schemes() []string
// 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 {
-97
View File
@@ -1,97 +0,0 @@
package fetcher
import "testing"
func TestSchemeFilter_Match(t *testing.T) {
type fields struct {
Type FilterType
Pattern string
}
type args struct {
uri string
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "url match",
fields: fields{
Type: FilterTypeUrl,
Pattern: "https",
},
args: args{
uri: "https://github.com",
},
want: true,
},
{
name: "url not match",
fields: fields{
Type: FilterTypeUrl,
Pattern: "https",
},
args: args{
uri: "ftp://github.com",
},
want: false,
},
{
name: "file match",
fields: fields{
Type: FilterTypeFile,
Pattern: "torrent",
},
args: args{
uri: "d:/temp/test.torrent",
},
want: true,
},
{
name: "file not match",
fields: fields{
Type: FilterTypeFile,
Pattern: "torrent",
},
args: args{
uri: "d:/temp/test.txt",
},
want: false,
},
{
name: "base64 match",
fields: fields{
Type: FilterTypeBase64,
Pattern: "application/x-bittorrent",
},
args: args{
uri: "data:application/x-bittorrent;base64,xxx",
},
want: true,
},
{
name: "base64 not match",
fields: fields{
Type: FilterTypeBase64,
Pattern: "application/x-bittorrent",
},
args: args{
uri: "data:application/javascript;base64,xxx",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SchemeFilter{
Type: tt.fields.Type,
Pattern: tt.fields.Pattern,
}
if got := s.Match(tt.args.uri); got != tt.want {
t.Errorf("Match() = %v, want %v", got, tt.want)
}
})
}
}
-6
View File
@@ -3,10 +3,4 @@ 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"`
}
@@ -1,49 +0,0 @@
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)
}
}
}
+87 -284
View File
@@ -2,7 +2,6 @@ package bt
import (
"bytes"
"context"
"fmt"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
@@ -11,7 +10,6 @@ import (
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"net/url"
"path/filepath"
"strings"
"sync"
@@ -24,8 +22,6 @@ var (
lock sync.Mutex
torrentDirMap = make(map[string]string)
ftMap = make(map[string]*fileTorrentImpl)
closeCtx context.Context
closeFunc func()
)
type Fetcher struct {
@@ -34,13 +30,15 @@ type Fetcher struct {
torrent *torrent.Torrent
meta *fetcher.FetcherMeta
data *fetcherData
torrentReady atomic.Bool
torrentUpload atomic.Bool
torrentDropCtx context.Context
torrentDropFunc func()
uploadDoneCh chan any
torrentReady atomic.Bool
torrentDrop atomic.Bool
create atomic.Bool
progress fetcher.Progress
}
func (f *Fetcher) Name() string {
return "bt"
}
func (f *Fetcher) Setup(ctl *controller.Controller) {
@@ -48,12 +46,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
if f.data == nil {
f.data = &fetcherData{}
exist := f.ctl.GetConfig(&f.config)
if !exist {
f.config = &config{
ListenPort: 0,
Trackers: []string{},
}
}
f.uploadDoneCh = make(chan any, 1)
f.torrentDropCtx, f.torrentDropFunc = context.WithCancel(context.Background())
f.ctl.GetConfig(&f.config)
return
}
@@ -64,16 +63,12 @@ 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
cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
cfg.HTTPProxy = f.ctl.ProxyConfig.ToHandler()
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
@@ -83,17 +78,7 @@ 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
}
@@ -101,42 +86,42 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
f.meta.Req = req
if err := f.addTorrent(req, false); err != nil {
if err := f.addTorrent(req); 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] = opts.Path
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
}
return nil
}
func (f *Fetcher) Start() (err error) {
if !f.torrentReady.Load() {
if err = f.addTorrent(f.meta.Req, false); err != nil {
if err = f.addTorrent(f.meta.Req); err != nil {
return
}
}
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
ft.setTorrentDir(f.meta.Opts.Path)
ft.setTorrentDir(f.meta.FolderPath())
}
files := f.torrent.Files()
// If the user does not specify the file to download, all files will be downloaded by default
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
}
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 {
@@ -145,6 +130,7 @@ func (f *Fetcher) Start() (err error) {
file.Download()
}
}
return
}
@@ -155,12 +141,8 @@ 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
}
@@ -173,94 +155,80 @@ 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 {
var stats torrent.TorrentStats
if f.torrent != nil {
stats = f.torrent.Stats()
} else {
stats = torrent.TorrentStats{}
}
return &bt.Stats{
stats := f.torrent.Stats()
baseStats := &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.data.Progress
return f.progress
}
for i := range f.data.Progress {
for i := range f.progress {
selectIndex := f.meta.Opts.SelectFiles[i]
file := f.torrent.Files()[selectIndex]
f.data.Progress[i] = file.BytesCompleted()
f.progress[i] = file.BytesCompleted()
}
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
return f.progress
}
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(),
}
// Directory torrent
if f.torrent.Info().Length == 0 {
res.Name = f.torrent.Name()
}
f.torrent.PeerConns()
for i, file := range f.torrent.Files() {
res.Files[i] = &base.FileInfo{
Name: filepath.Base(file.DisplayPath()),
Path: util.Dir(file.DisplayPath()),
Path: util.Dir(file.Path()),
Size: file.Length(),
}
}
@@ -271,84 +239,14 @@ func (f *Fetcher) updateRes() {
}
}
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) {
func (f *Fetcher) addTorrent(req *base.Request) (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 == "DATA" {
} else if schema == "APPLICATION/X-BITTORRENT" {
_, data := util.ParseDataUri(req.URL)
buf := bytes.NewBuffer(data)
var metaInfo *metainfo.MetaInfo
@@ -388,129 +286,34 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
}
<-f.torrent.GotInfo()
f.torrentReady.Store(true)
go f.doUpload(fromUpload)
return
}
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)
type FetcherBuilder struct {
}
type fetcherData struct {
Progress fetcher.Progress
SeedBytes int64
// SeedTime is the time in seconds to seed after downloading is complete.
SeedTime int64
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
func (fb *FetcherBuilder) Schemes() []string {
return schemes
}
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 {
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
return &Fetcher{}
}
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) Store(f fetcher.Fetcher) (data any, err error) {
return nil, nil
}
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 {
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 {
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, ".", "")
+31 -126
View File
@@ -8,8 +8,6 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
gohttp "net/http"
"net/url"
"os"
"reflect"
"testing"
@@ -35,6 +33,7 @@ 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{
@@ -70,151 +69,57 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
}
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
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",
},
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)
}
want := &base.Resource{
Size: 1466714112,
Range: true,
Files: []*base.FileInfo{
{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
},
},
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 err != nil {
panic(err)
}
tests := []struct {
name string
args args
want string
}{
{
name: "broken url",
args: args{
u: "magnet://!@#%github.com",
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: "",
},
{
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: "",
},
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
}
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)
}
})
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
}
}
func buildFetcher() fetcher.Fetcher {
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)
fetcher := new(FetcherBuilder).Build()
fetcher.Setup(controller.NewController())
return fetcher
}
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
fetcher := new(FetcherManager).Build()
fetcher := new(FetcherBuilder).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) {
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
}
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
return proxyConfig.ToHandler()
newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
return false
}
return true
}
newController.ProxyConfig = proxyConfig
fetcher.Setup(newController)
return fetcher
}
Binary file not shown.
+35 -72
View File
@@ -5,6 +5,12 @@ 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"
@@ -15,13 +21,6 @@ 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 {
@@ -65,13 +64,23 @@ 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{}
}
f.ctl.GetConfig(&f.config)
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,
}
}
return
}
@@ -79,7 +88,6 @@ 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
@@ -100,7 +108,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)) {
// response 206 status code, support breakpoint continuation
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
res.Range = true
// 解析资源大小: bytes 0-1000/1001 => 1001
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
@@ -112,8 +120,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
res.Size = parse
}
} else if base.HttpCodeOK == httpResp.StatusCode {
// response 200 status code, not support breakpoint continuation, get file size by Content-Length header
// if not found, maybe chunked encoding
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
if contentLength != "" {
parse, err := strconv.ParseInt(contentLength, 10, 64)
@@ -154,6 +161,7 @@ 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
}
@@ -171,12 +179,8 @@ 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
}
@@ -382,8 +386,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
method string
body io.Reader
)
headers := http.Header{}
headers := make(map[string][]string)
if req.Extra == nil {
method = http.MethodGet
} else {
@@ -395,7 +398,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
}
if len(extra.Header) > 0 {
for k, v := range extra.Header {
headers.Set(k, v)
headers[k] = []string{v}
}
}
if extra.Body != "" {
@@ -403,7 +406,8 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
}
}
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
// load user agent from config
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
}
if ctx != nil {
@@ -448,7 +452,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
func (f *Fetcher) buildClient() *http.Client {
transport := &http.Transport{
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
Proxy: f.ctl.ProxyConfig.ToHandler(),
}
// Cookie handle
jar, _ := cookiejar.New(nil)
@@ -462,67 +466,30 @@ type fetcherData struct {
Chunks []*chunk
}
type FetcherManager struct {
type FetcherBuilder struct {
}
func (fm *FetcherManager) Name() string {
return "http"
var schemes = []string{"HTTP", "HTTPS"}
func (fb *FetcherBuilder) Schemes() []string {
return schemes
}
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 {
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
return &Fetcher{}
}
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) {
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
_f := f.(*Fetcher)
return &fetcherData{
Chunks: _f.chunks,
}, nil
}
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
func (fb *FetcherBuilder) 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 := &FetcherManager{}
fb := &FetcherBuilder{}
fetcher := fb.Build().(*Fetcher)
fetcher.meta = meta
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
@@ -533,7 +500,3 @@ func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
return fetcher
}
}
func (fm *FetcherManager) Close() error {
return nil
}
+13 -67
View File
@@ -9,8 +9,6 @@ import (
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"net"
gohttp "net/http"
"net/url"
"testing"
"time"
)
@@ -194,54 +192,6 @@ 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)
}
@@ -356,7 +306,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
t.Fatal(err)
}
fb := new(FetcherManager)
fb := new(FetcherBuilder)
time.Sleep(time.Millisecond * 50)
data, err := fb.Store(fetcher)
if err != nil {
@@ -387,12 +337,10 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
fetcher := downloadReady(httpListener, 4, t)
ctl := controller.NewController()
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
return (&base.DownloaderProxyConfig{
Enable: true,
Scheme: "socks5",
Host: proxyListener.Addr().String(),
}).ToHandler()
ctl.ProxyConfig = &base.DownloaderProxyConfig{
Enable: true,
Scheme: "socks5",
Host: proxyListener.Addr().String(),
}
fetcher.Setup(ctl)
err := fetcher.Start()
@@ -411,21 +359,19 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
}
func buildFetcher() *Fetcher {
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)
fetcher := new(FetcherBuilder).Build()
fetcher.Setup(controller.NewController())
return fetcher.(*Fetcher)
}
func buildConfigFetcher(cfg config) fetcher.Fetcher {
fetcher := new(FetcherManager).Build()
fetcher := new(FetcherBuilder).Build()
newController := controller.NewController()
newController.GetConfig = func(v any) {
json.Unmarshal([]byte(test.ToJson(cfg)), v)
newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
return false
}
return true
}
fetcher.Setup(newController)
return fetcher
+1 -1
View File
@@ -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"
+6 -59
View File
@@ -16,8 +16,6 @@ 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 {
@@ -27,37 +25,6 @@ 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
@@ -127,7 +94,12 @@ func (o *Options) InitSelectFiles(fileSize int) {
}
func (o *Options) Clone() *Options {
return util.DeepClone(o)
return &Options{
Name: o.Name,
Path: o.Path,
SelectFiles: o.SelectFiles,
Extra: o.Extra,
}
}
func ParseReqExtra[E any](req *Request) error {
@@ -175,37 +147,12 @@ 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
-264
View File
@@ -1,264 +0,0 @@
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)
}
})
}
}
+119 -240
View File
@@ -12,8 +12,6 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"github.com/virtuald/go-paniclog"
gohttp "net/http"
"net/url"
"os"
"path/filepath"
"sort"
@@ -44,29 +42,25 @@ 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
fetcherCache map[string]fetcher.Fetcher
storage Storage
tasks []*Task
waitTasks []*Task
watchedTasks sync.Map
listener Listener
cfg *DownloaderConfig
fetcherBuilders map[string]fetcher.FetcherBuilder
fetcherCache map[string]fetcher.Fetcher
storage Storage
tasks []*Task
waitTasks []*Task
listener Listener
lock *sync.Mutex
fetcherMapLock *sync.RWMutex
@@ -83,10 +77,11 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
cfg.Init()
d := &Downloader{
cfg: cfg,
fetcherCache: make(map[string]fetcher.Fetcher),
waitTasks: make([]*Task, 0),
storage: cfg.Storage,
cfg: cfg,
fetcherBuilders: make(map[string]fetcher.FetcherBuilder),
fetcherCache: make(map[string]fetcher.Fetcher),
waitTasks: make([]*Task, 0),
storage: cfg.Storage,
lock: &sync.Mutex{},
fetcherMapLock: &sync.RWMutex{},
@@ -94,6 +89,11 @@ 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"))
@@ -124,14 +124,11 @@ func (d *Downloader) Setup() error {
}
// init default config
d.cfg.DownloaderStoreConfig.Init()
// 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()
}
}
for _, fb := range d.fetcherBuilders {
f := fb.Build()
d.setupFetcher(f)
}
// load tasks from storage
var tasks []*Task
if err = d.storage.List(bucketTask, &tasks); err != nil {
@@ -169,22 +166,6 @@ 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() {
@@ -193,31 +174,22 @@ func (d *Downloader) Setup() error {
func() {
task.statusLock.Lock()
defer task.statusLock.Unlock()
if task.Status != base.DownloadStatusRunning && !task.Uploading {
if task.Status != base.DownloadStatusRunning {
return
}
// check if task is deleted
if d.GetTask(task.ID) == nil || task.fetcher == nil {
if d.GetTask(task.ID) == nil {
return
}
current := task.fetcher.Progress().TotalDownloaded()
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
}
task.Progress.Used = task.timer.Used()
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000)
task.Progress.Downloaded = current
d.emit(EventKeyProgress, task)
// store fetcher progress
data, err := task.fetcherManager.Store(task.fetcher)
data, err := task.fetcherBuilder.Store(task.fetcher)
if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
return
@@ -239,36 +211,21 @@ func (d *Downloader) Setup() error {
return 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
}
}
func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
schema := util.ParseSchema(url)
fetchBuilder, ok := d.fetcherBuilders[schema]
if ok {
return fetchBuilder, nil
}
return nil, ErrUnSupportedProtocol
}
func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fetcher) {
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
ctl := controller.NewController()
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.GetConfig = func(v any) bool {
return d.getProtocolConfig(fetcher.Name(), v)
}
ctl.ProxyConfig = d.cfg.Proxy
fetcher.Setup(ctl)
}
@@ -344,18 +301,6 @@ 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]
@@ -377,10 +322,18 @@ func (d *Downloader) Pause(id string) (err error) {
return ErrTaskNotFound
}
if err = d.doPause(task); err != nil {
return
}
d.notifyRunning()
func() {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusPause {
return
}
if err = d.doPause(task); err != nil {
return
}
d.notifyRunning()
}()
return
}
@@ -499,10 +452,8 @@ 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()
@@ -524,7 +475,7 @@ func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err err
}
}()
for _, task := range deleteTasksPtr {
for _, task := range deleteTasks {
err = d.doDelete(task, force)
if err != nil {
return
@@ -540,17 +491,6 @@ 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
}
@@ -596,23 +536,10 @@ func (d *Downloader) doDelete(task *Task, force bool) (err error) {
func (d *Downloader) Close() error {
d.closed.Store(true)
closeArr := []func() error{
d.PauseAll,
if err := d.PauseAll(); err != nil {
return err
}
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
return d.storage.Close()
}
func (d *Downloader) Clear() error {
@@ -702,49 +629,11 @@ 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()
@@ -761,7 +650,6 @@ 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()
@@ -795,51 +683,46 @@ func (d *Downloader) doOnError(task *Task, err error) {
}
}
func (d *Downloader) restoreTask(task *Task) error {
func (d *Downloader) restoreFetcher(task *Task) error {
if task.fetcher == nil {
if err := d.restoreFetcher(task); err != nil {
fb, err := d.parseFb(task.Meta.Req.URL)
if 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) 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) {
func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (taskId string, err error) {
if opts == nil {
opts = &base.Options{}
}
@@ -847,7 +730,7 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
opts.SelectFiles = make([]int, 0)
}
meta := f.Meta()
meta := fetcher.Meta()
meta.Opts = opts
if opts.Path == "" {
storeConfig, err := d.GetConfig()
@@ -857,20 +740,19 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
opts.Path = storeConfig.DownloadDir
}
fm, err := d.parseFm(f.Meta().Req.URL)
fb, err := d.parseFb(fetcher.Meta().Req.URL)
if err != nil {
return
}
task := NewTask()
task.fetcherManager = fm
task.fetcher = f
task.Protocol = fm.Name()
task.Meta = f.Meta()
task.fetcherBuilder = fb
task.fetcher = fetcher
task.Protocol = fetcher.Name()
task.Meta = fetcher.Meta()
task.Progress = &Progress{}
_, task.Uploading = f.(fetcher.Uploader)
initTask(task)
if err = f.Create(opts); err != nil {
if err = fetcher.Create(opts); err != nil {
return
}
if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
@@ -913,7 +795,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
return
}
err = d.restoreTask(task)
err = d.restoreFetcher(task)
if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
return
@@ -946,28 +828,26 @@ func (d *Downloader) doStart(task *Task) (err error) {
}
if isCreate {
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
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.Res.CalcSize(task.Meta.Opts.SelectFiles)
@@ -975,10 +855,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
task.Progress.Speed = 0
task.timer.Start()
if err := task.fetcher.Start(); err != nil {
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err
}
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
if err := task.fetcher.Start(); err != nil {
return err
}
d.emit(EventKeyStart, task)
@@ -1048,12 +928,12 @@ func logPanic(logDir string) {
}
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
fm, err := d.parseFm(url)
fb, err := d.parseFb(url)
if err != nil {
return nil, err
}
fetcher := fm.Build()
d.setupFetcher(fm, fetcher)
fetcher := fb.Build()
d.setupFetcher(fetcher)
return fetcher, nil
}
@@ -1063,7 +943,6 @@ 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)
+14 -116
View File
@@ -5,8 +5,6 @@ import (
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
@@ -86,94 +84,23 @@ func TestDownloader_Create(t *testing.T) {
}
}
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()
}
})
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
})
if err != nil {
t.Fatal(err)
}
wg.Wait()
tasks := downloader.GetTasks()
if len(tasks) != len(reqs) {
t.Errorf("CreateDirectBatch() task got = %v, want %v", len(tasks), len(reqs))
}
nameMatchMap := make(map[string]bool)
for _, name := range fileNames {
if _, err := os.Stat(test.Dir + "/" + name); os.IsNotExist(err) {
t.Errorf("CreateDirectBatch() file not exist: %v", name)
}
for _, task := range tasks {
if name == task.Meta.Opts.Name {
nameMatchMap[task.Meta.Opts.Name] = true
}
}
}
if len(nameMatchMap) != len(reqs) {
t.Errorf("CreateDirectBatch() names got = %v, want %v", len(nameMatchMap), len(reqs))
}
}
func TestDownloader_CreateWithProxy(t *testing.T) {
// No proxy
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return nil
}, nil)
// Disable proxy
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
doTestDownloaderCreateWithProxy(t, false, 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 {
doTestDownloaderCreateWithProxy(t, false, 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 {
doTestDownloaderCreateWithProxy(t, false, 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
@@ -184,50 +111,33 @@ func TestDownloader_CreateWithProxy(t *testing.T) {
}
})
// Enable system proxy and set proxy environment variable
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
doTestDownloaderCreateWithProxy(t, false, 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 {
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Scheme = ""
return proxyCfg
}, nil)
// Invalid proxy host
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
doTestDownloaderCreateWithProxy(t, false, 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 {
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return proxyCfg
}, nil)
// Use proxy with auth
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
doTestDownloaderCreateWithProxy(t, true, 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)) {
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
usr, pwd := "", ""
if auth {
usr, pwd = "admin", "123"
@@ -240,29 +150,17 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func
t.Fatal(err)
}
defer downloader.Clear()
globalProxyCfg := &base.DownloaderProxyConfig{
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&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: 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 {
if errHandler == nil {
@@ -409,8 +307,8 @@ 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, true)
if exits {
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
}
storeCfg := &base.DownloaderStoreConfig{
+2 -14
View File
@@ -106,7 +106,7 @@ func (e *Engine) await(value any) {
}
func (e *Engine) Close() {
e.loop.StopNoWait()
e.loop.Stop()
}
type Config struct {
@@ -145,10 +145,6 @@ 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
@@ -178,15 +174,7 @@ func resolveResult(value goja.Value) (any, error) {
if err, ok := p.Result().Export().(error); ok {
return nil, err
} else {
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)
return nil, errors.New(p.Result().String())
}
}
}
+8 -111
View File
@@ -6,6 +6,12 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
"io"
"net"
"net/http"
@@ -13,13 +19,6 @@ 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) {
@@ -27,8 +26,6 @@ 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")
}
@@ -72,44 +69,6 @@ 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');
@@ -121,30 +80,6 @@ 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();
@@ -237,20 +172,6 @@ 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)
@@ -269,19 +190,14 @@ 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", err)
t.Fatal("progress test failed")
}
_, err = callTestFun(engine, "testAbort")
if err != nil {
t.Fatal("abort test failed", err)
t.Fatal("abort test failed")
}
_, err = callTestFun(engine, "testTimeout")
@@ -412,15 +328,6 @@ 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)
@@ -460,16 +367,6 @@ 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
+42 -72
View File
@@ -3,18 +3,16 @@ package xhr
import (
"bytes"
"errors"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
"io"
"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 (
@@ -26,12 +24,6 @@ const (
eventTimeout = "timeout"
)
const (
redirectError = "error"
redirectFollow = "follow"
redirectManual = "manual"
)
type ProgressEvent struct {
Type string `json:"type"`
LengthComputable bool `json:"lengthComputable"`
@@ -126,26 +118,18 @@ type XMLHttpRequestUpload struct {
type XMLHttpRequest struct {
method string
url string
requestHeaders http.Header
responseHeaders http.Header
requestHeaders map[string]string
responseHeaders map[string]string
aborted bool
proxyHandler func(r *http.Request) (*url.URL, error)
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"`
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"`
*EventProp
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
}
@@ -153,13 +137,13 @@ type XMLHttpRequest struct {
func (xhr *XMLHttpRequest) Open(method, url string) {
xhr.method = method
xhr.url = url
xhr.requestHeaders = make(http.Header)
xhr.responseHeaders = make(http.Header)
xhr.requestHeaders = make(map[string]string)
xhr.responseHeaders = make(map[string]string)
xhr.doReadystatechange(1)
}
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
xhr.requestHeaders.Add(key, value)
xhr.requestHeaders[key] = value
}
func (xhr *XMLHttpRequest) Send(data goja.Value) {
@@ -169,33 +153,31 @@ 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 v := d.(type) {
switch d.(type) {
case string:
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string)))
contentType = "text/plain;charset=UTF-8"
contentLength = int64(len(v))
isStringBody = true
contentLength = int64(len(d.(string)))
case *file.File:
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader)
contentType = "application/octet-stream"
contentLength = v.Size
contentLength = d.(*file.File).Size
case *formdata.FormData:
pr, pw := io.Pipe()
mw := NewMultipart(pw)
for _, e := range v.Entries() {
for _, e := range d.(*formdata.FormData).Entries() {
arr := e.([]any)
k := arr[0].(string)
v := arr[1]
switch v := v.(type) {
switch v.(type) {
case string:
mw.WriteField(k, v)
mw.WriteField(k, v.(string))
case *file.File:
mw.WriteFile(k, v)
mw.WriteFile(k, v.(*file.File))
}
}
go func() {
@@ -212,32 +194,21 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
xhr.callOnerror()
return
}
req.Header = xhr.requestHeaders
// Only string body can specify Content-Type header by user
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
if contentType != "" {
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{
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 {
@@ -259,12 +230,9 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
if !xhr.aborted {
xhr.Upload.callOnload()
}
responseUrl := resp.Request.URL
responseUrl.Fragment = ""
xhr.ResponseUrl = responseUrl.String()
xhr.responseHeaders = resp.Header
for k, v := range resp.Header {
xhr.responseHeaders[k] = v[0]
}
xhr.Status = resp.StatusCode
xhr.StatusText = resp.Status
xhr.doReadystatechange(2)
@@ -293,7 +261,7 @@ func (xhr *XMLHttpRequest) Abort() {
}
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
return strings.Join(xhr.responseHeaders.Values(key), ", ")
return xhr.responseHeaders[key]
}
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
@@ -301,7 +269,7 @@ func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
for k, v := range xhr.responseHeaders {
buf.WriteString(k)
buf.WriteString(": ")
buf.WriteString(strings.Join(v, ", "))
buf.WriteString(v)
buf.WriteString("\r\n")
}
return buf.String()
@@ -419,9 +387,10 @@ func (w *multipartWrapper) Size() int64 {
w.statWriter.Close()
size := int64(w.statBuffer.Len())
for _, v := range w.fields {
switch v := v.(type) {
switch v.(type) {
case *file.File:
size += v.Size
f := v.(*file.File)
size += f.Size
}
}
return size
@@ -429,17 +398,18 @@ func (w *multipartWrapper) Size() int64 {
func (w *multipartWrapper) Send() error {
for k, v := range w.fields {
switch v := v.(type) {
switch v.(type) {
case string:
if err := w.writer.WriteField(k, v); err != nil {
if err := w.writer.WriteField(k, v.(string)); err != nil {
return err
}
case *file.File:
fw, err := w.writer.CreateFormFile(k, v.Name)
f := v.(*file.File)
fw, err := w.writer.CreateFormFile(k, f.Name)
if err != nil {
return err
}
if _, err = io.Copy(fw, v); err != nil {
if _, err = io.Copy(fw, f); err != nil {
return err
}
}
+1 -1
View File
@@ -1 +1 @@
(()=>{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
(()=>{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
File diff suppressed because it is too large. Load diff
+2 -5
View File
@@ -7,19 +7,16 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode production --watch",
"build": "webpack --mode production",
"postinstall": "patch-package"
"build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"devDependencies": {
"patch-package": "^8.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"blob-polyfill": "^7.0.20220408",
"fastestsmallesttextencoderdecoder": "^1.0.22",
"whatwg-fetch": "^3.6.20"
"whatwg-fetch": "^3.6.2"
}
}
@@ -1,46 +0,0 @@
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,5 +1,3 @@
import "./blob/index.js"
import "./crypto/index.js"
// polyfill TextEncoder
import 'fastestsmallesttextencoderdecoder';
import "./fetch/index.js"
+6 -31
View File
@@ -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,13 +155,7 @@ func (d *Downloader) DeleteExtension(identity string) error {
break
}
}
if err = d.storage.Delete(bucketExtension, identity); err != nil {
return err
}
if err = d.storage.Delete(bucketExtensionStorage, identity); err != nil {
return err
}
return nil
return d.storage.Delete(bucketExtension, identity)
}
func (d *Downloader) GetExtensions() []*Extension {
@@ -304,17 +298,6 @@ 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{
@@ -622,9 +605,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
h.register(EventOnError, fn)
}
func (h InstanceEvents) OnDone(fn goja.Callable) {
h.register(EventOnDone, fn)
}
//func (h InstanceEvents) OnDone(fn goja.Callable) {
// h.register(HookEventOnDone, fn)
//}
type ExtensionInfo struct {
Identity string `json:"identity"`
@@ -698,10 +681,6 @@ type OnErrorContext struct {
Error error `json:"error"`
}
type OnDoneContext struct {
Task *Task `json:"task"`
}
type ExtensionTask struct {
*Task
@@ -709,12 +688,8 @@ 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: newTask,
Task: task.clone(),
download: download,
}
}
+2 -39
View File
@@ -247,45 +247,8 @@ func TestDownloader_Extension_OnError(t *testing.T) {
}
// extension on error modify url and continue download
task := downloader.GetTask(id)
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.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)
+21 -58
View File
@@ -1,7 +1,6 @@
package download
import (
"encoding/json"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/protocol/bt"
@@ -20,21 +19,19 @@ type ResolveResult struct {
type Task struct {
ID string `json:"id"`
Protocol string `json:"protocol"`
Meta *fetcher.FetcherMeta `json:"meta"`
Status base.Status `json:"status"`
Uploading bool `json:"uploading"`
Protocol string `json:"protocol"`
Progress *Progress `json:"progress"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
fetcherManager fetcher.FetcherManager
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher
timer *util.Timer
statusLock *sync.Mutex
lock *sync.Mutex
speedArr []int64
uploadSpeedArr []int64
}
func NewTask() *Task {
@@ -50,71 +47,37 @@ 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 util.DeepClone(t)
return &Task{
ID: t.ID,
Meta: t.Meta,
Status: t.Status,
Progress: t.Progress,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
speedArr = append(speedArr, downloaded)
if len(speedArr) > 6 {
speedArr = speedArr[1:]
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:]
}
var total int64
for _, v := range speedArr {
for _, v := range t.speedArr {
total += v
}
return int64(float64(total) / float64(len(speedArr)) / usedTime)
return int64(float64(total) / float64(len(t.speedArr)) / usedTime)
}
type DownloaderConfig struct {
Controller *controller.Controller
FetchManagers []fetcher.FetcherManager
FetchBuilders []fetcher.FetcherBuilder
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
@@ -129,10 +92,10 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
if cfg.Controller == nil {
cfg.Controller = controller.NewController()
}
if len(cfg.FetchManagers) == 0 {
cfg.FetchManagers = []fetcher.FetcherManager{
new(http.FetcherManager),
new(bt.FetcherManager),
if len(cfg.FetchBuilders) == 0 {
cfg.FetchBuilders = []fetcher.FetcherBuilder{
new(http.FetcherBuilder),
new(bt.FetcherBuilder),
}
}
if cfg.RefreshInterval == 0 {
+1 -3
View File
@@ -69,9 +69,7 @@ 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()
if bucketData, ok := memData[bucket]; ok {
bucketData[key] = v
}
memData[bucket][key] = v
return nil
}
-5
View File
@@ -1,5 +0,0 @@
gopeed.events.onDone(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
ctx.task.meta.req.labels['modified'] = 'true';
});
-16
View File
@@ -1,16 +0,0 @@
{
"name": "on-done",
"title": "gopeed extension on done event test",
"version": "0.0.1",
"scripts": [
{
"event": "onDone",
"match": {
"urls": [
"*://github.com/*"
]
},
"entry": "index.js"
}
]
}
+1
View File
@@ -2,6 +2,7 @@ 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();
});
+2 -7
View File
@@ -4,16 +4,11 @@ type ReqExtra struct {
Trackers []string `json:"trackers"`
}
// Stats for torrent
// Stats for download
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"`
}
-28
View File
@@ -8,20 +8,8 @@ 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) {
@@ -57,22 +45,6 @@ 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"]
+7
View File
@@ -0,0 +1,7 @@
package model
type Command struct {
Protocol string `json:"protocol"`
Action string `json:"action"`
Params any `json:"params"`
}
+6 -8
View File
@@ -2,7 +2,6 @@ package model
import (
"encoding/base64"
"github.com/GopeedLab/gopeed/pkg/base"
"io/fs"
)
@@ -14,13 +13,12 @@ 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"`
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
Network string `json:"network"`
Address string `json:"address"`
RefreshInterval int `json:"refreshInterval"`
Storage Storage `json:"storage"`
StorageDir string `json:"storageDir"`
ApiToken string `json:"apiToken"`
ProductionMode bool
-5
View File
@@ -7,8 +7,3 @@ 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"`
}
+8 -21
View File
@@ -19,42 +19,31 @@ import (
)
var (
srv *http.Server
runningPort int
srv *http.Server
Downloader *download.Downloader
)
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)
func Start(startCfg *model.StartConfig) (int, error) {
srv, listener, err := BuildServer(startCfg)
if err != nil {
return
return 0, err
}
go func() {
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
port := 0
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
port = addr.Port
runningPort = port
}
return
return port, nil
}
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")
@@ -99,10 +88,8 @@ 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)
@@ -154,7 +141,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).Msgf("http server panic: %s %s", r.Method, r.RequestURI)
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
}
}()
+9 -28
View File
@@ -63,18 +63,6 @@ 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)
@@ -137,21 +125,6 @@ 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
@@ -583,7 +556,15 @@ func doTest(handler func()) {
}()
defer func() {
Downloader.PauseAll()
Downloader.DeleteByStatues(nil, true)
tasks := Downloader.GetTasks()
taskIds := make([]string, len(tasks))
for i, task := range tasks {
taskIds[i] = task.ID
}
for _, id := range taskIds {
Downloader.Delete(id, true)
}
os.RemoveAll(cfg.StorageDir)
}()
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
-14
View File
@@ -12,17 +12,3 @@ 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
}
-60
View File
@@ -1,60 +0,0 @@
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)
}
})
}
}
+7 -1
View File
@@ -8,13 +8,18 @@ import (
"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 ""
return FileSchema
}
schema := url[:index]
if schema == "data" {
schema, _ = ParseDataUri(url)
}
return strings.ToUpper(schema)
}
@@ -27,6 +32,7 @@ func ParseDataUri(uri string) (string, []byte) {
}
mime := matches[1]
base64Data := matches[2]
// 解码Base64数据
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "", nil
+3 -3
View File
@@ -41,21 +41,21 @@ func TestParseSchema(t *testing.T) {
args: args{
url: "./url.go",
},
want: "",
want: "FILE",
},
{
name: "data-uri",
args: args{
url: "data:application/x-bittorrent;base64,test",
},
want: "DATA",
want: "APPLICATION/X-BITTORRENT",
},
{
name: "windows-path",
args: args{
url: "D:\\bt.torrent",
},
want: "",
want: "FILE",
},
}
for _, tt := range tests {
+1 -1
View File
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 34
compileSdkVersion 33
ndkVersion flutter.ndkVersion
compileOptions {
@@ -2,19 +2,12 @@
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:extractNativeLibs="true">
android:label="Gopeed">
<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: 63 KiB

After

Width:  |  Height:  |  Size: 181 KiB

@@ -13,7 +13,6 @@
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, ); }; };
@@ -35,7 +34,6 @@
/* 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>"; };
@@ -65,7 +63,6 @@
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */,
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */,
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */,
B44F54F47E581A39546303C9 /* libresolv.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -133,7 +130,6 @@
D0E623A729482D030001185E /* Libgopeed.xcframework */,
D081C25B294826C0006EB10B /* libc++.tbd */,
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */,
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -17,7 +17,6 @@ class DownloaderConfig {
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
_$DownloaderConfigFromJson(json);
Map<String, dynamic> toJson() => _$DownloaderConfigToJson(this);
}
@@ -30,7 +29,6 @@ class ProtocolConfig {
factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
json == null ? ProtocolConfig() : _$ProtocolConfigFromJson(json);
Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this);
}
@@ -41,58 +39,39 @@ class HttpConfig {
bool useServerCtime;
HttpConfig({
this.userAgent = '',
this.connections = 0,
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.useServerCtime = false,
});
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
_$HttpConfigFromJson(json);
Map<String, dynamic> toJson() => _$HttpConfigToJson(this);
}
@JsonSerializable()
class BtConfig {
int listenPort;
List<String> trackers;
bool seedKeep;
double seedRatio;
int seedTime;
int listenPort = 0;
List<String> trackers = [];
BtConfig({
this.listenPort = 0,
this.trackers = const [],
this.seedKeep = false,
this.seedRatio = 0,
this.seedTime = 0,
});
BtConfig();
factory BtConfig.fromJson(Map<String, dynamic> json) =>
_$BtConfigFromJson(json);
Map<String, dynamic> toJson() => _$BtConfigToJson(this);
}
@JsonSerializable(explicitToJson: true)
class ExtraConfig {
String themeMode;
String locale;
bool lastDeleteTaskKeep;
bool defaultDirectDownload;
String themeMode = '';
String locale = '';
ExtraConfigBt bt = ExtraConfigBt();
ExtraConfig({
this.themeMode = '',
this.locale = '',
this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false,
});
ExtraConfig();
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
json == null ? ExtraConfig() : _$ExtraConfigFromJson(json);
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
}
@@ -133,6 +112,5 @@ class ExtraConfigBt {
factory ExtraConfigBt.fromJson(Map<String, dynamic> json) =>
_$ExtraConfigBtFromJson(json);
Map<String, dynamic> toJson() => _$ExtraConfigBtToJson(this);
}
@@ -37,8 +37,9 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
};
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
userAgent: json['userAgent'] as String? ?? '',
connections: (json['connections'] as num?)?.toInt() ?? 0,
userAgent: json['userAgent'] as String? ??
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
connections: (json['connections'] as num?)?.toInt() ?? 16,
useServerCtime: json['useServerCtime'] as bool? ?? false,
);
@@ -49,38 +50,25 @@ Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
'useServerCtime': instance.useServerCtime,
};
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,
);
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
..listenPort = (json['listenPort'] as num).toInt()
..trackers =
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
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? ?? '',
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
)..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
..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(),
};
+2 -2
View File
@@ -10,7 +10,7 @@ Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
name: json['name'] as String,
path: json['path'] as String,
selectFiles: (json['selectFiles'] as List<dynamic>?)
?.map((e) => e as int)
?.map((e) => (e as num).toInt())
.toList() ??
const [],
extra: json['extra'],
@@ -35,7 +35,7 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
OptsExtraHttp()
..connections = json['connections'] as int
..connections = (json['connections'] as num).toInt()
..autoTorrent = json['autoTorrent'] as bool;
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
-23
View File
@@ -7,13 +7,11 @@ 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) =>
@@ -47,24 +45,3 @@ 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);
}
-26
View File
@@ -12,9 +12,6 @@ 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) {
@@ -30,7 +27,6 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
writeNotNull('extra', instance.extra);
writeNotNull('labels', instance.labels);
writeNotNull('proxy', instance.proxy?.toJson());
return val;
}
@@ -54,25 +50,3 @@ 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',
};
+1 -1
View File
@@ -11,7 +11,7 @@ Result<T> _$ResultFromJson<T>(
T Function(Object? json) fromJsonT,
) =>
Result<T>(
code: json['code'] as int,
code: (json['code'] as num).toInt(),
msg: json['msg'] as String?,
data: _$nullableGenericFromJson(json['data'], fromJsonT),
);
-11
View File
@@ -6,26 +6,19 @@ 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,
@@ -41,15 +34,11 @@ 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) =>
+9 -33
View File
@@ -8,36 +8,21 @@ 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) {
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;
}
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(),
};
const _$StatusEnumMap = {
Status.ready: 'ready',
@@ -48,23 +33,14 @@ const _$StatusEnumMap = {
Status.done: 'done',
};
const _$ProtocolEnumMap = {
Protocol.http: 'http',
Protocol.bt: 'bt',
};
Progress _$ProgressFromJson(Map<String, dynamic> json) => Progress(
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:launch_at_startup/launch_at_startup.dart';
import 'package:gopeed/database/entity.dart';
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.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,11 +17,8 @@ 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';
@@ -76,7 +73,6 @@ 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;
@@ -102,16 +98,12 @@ 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
@@ -122,12 +114,6 @@ 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();
@@ -195,20 +181,10 @@ 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 => {
@@ -216,6 +192,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
await Get.rootDelegate.offAndToNamed(Routes.CREATE),
},
),
MenuItem.separator(),
MenuItem(
label: "startAll".tr,
onClick: (menuItem) async => {continueAllTasks()},
@@ -245,14 +222,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
MenuItem.separator(),
MenuItem(
label: 'exit'.tr,
onClick: (menuItem) async {
try {
await LibgopeedBoot.instance.stop();
} catch (e) {
logger.w("libgopeed stop fail", e);
}
windowManager.destroy();
},
onClick: (menuItem) => {windowManager.destroy()},
),
]);
if (!Util.isLinux()) {
@@ -270,12 +240,16 @@ 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,
),
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',
)),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: true,
playSound: false,
@@ -295,19 +269,12 @@ 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);
@@ -338,24 +305,22 @@ class AppController extends GetxController with WindowListener, TrayListener {
return _defaultStartConfig!;
}
Future<StartConfig> loadStartConfig() async {
Future<void> 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<DownloaderConfig> loadDownloaderConfig() async {
Future<void> loadDownloaderConfig() async {
try {
downloaderConfig.value = await getConfig();
} catch (e) {
logger.w("load downloader config fail", e, StackTrace.current);
logger.w("load downloader config fail", e);
downloaderConfig.value = DownloaderConfig();
}
await _initDownloaderConfig();
return downloaderConfig.value;
}
Future<void> trackerUpdate() async {
@@ -448,8 +413,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
if (Util.isDesktop()) {
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
} else if (Util.isAndroid()) {
config.downloadDir = (await getExternalStorageDirectory())?.path ??
(await getApplicationDocumentsDirectory()).path;
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
if (downloadDir != null) {
config.downloadDir = '$downloadDir/Gopeed';
} else {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
}
} else if (Util.isIOS()) {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
} else {
@@ -458,17 +427,6 @@ 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,9 +3,6 @@ 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 {
@@ -16,7 +13,6 @@ 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;
@@ -25,11 +21,6 @@ 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,7 +4,6 @@ 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';
@@ -18,7 +17,6 @@ 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';
@@ -33,10 +31,6 @@ 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();
@@ -266,164 +260,7 @@ 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 [
@@ -463,7 +300,7 @@ class CreateView extends GetView<CreateController> {
controller: _btTrackerController,
maxLines: 5,
decoration: InputDecoration(
labelText: 'Trackers',
labelText: 'Trakers',
hintText: 'addTrackerHit'.tr,
)),
],
@@ -484,13 +321,23 @@ class CreateView extends GetView<CreateController> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
CompactCheckbox(
label: 'directDownload'.tr,
value: controller.directDownload.value,
onChanged: (bool? value) {
controller.directDownload.value =
value ?? false;
}),
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),
]),
),
TextButton(
onPressed: () {
controller.showAdvanced.value =
@@ -558,7 +405,7 @@ class CreateView extends GetView<CreateController> {
Util.isWeb() && controller.fileDataUri.isNotEmpty;
final submitUrl = isWebFileChosen
? controller.fileDataUri.value
: _urlController.text.trim();
: _urlController.text;
final urls = Util.textToLines(submitUrl);
// Add url to the history
@@ -568,7 +415,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
@@ -578,21 +425,18 @@ class CreateView extends GetView<CreateController> {
if (isDirect) {
await Future.wait(urls.map((url) {
return createTask(CreateTask(
req: Request(
url: url, extra: parseReqExtra(url), proxy: parseProxy()),
req: Request(url: url, extra: parseReqExtra(url)),
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);
}
@@ -605,39 +449,20 @@ 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) {
switch (controller.advancedTabController.index) {
case 0:
final header = {
final u = Uri.parse(_urlController.text);
if (u.scheme.startsWith("http")) {
reqExtra = ReqExtraHttp()
..header = {
"User-Agent": _httpUaController.text,
"Cookie": _httpCookieController.text,
"Referer": _httpRefererController.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;
} else {
reqExtra = ReqExtraBt()
..trackers = Util.textToLines(_btTrackerController.text);
}
}
return reqExtra;
@@ -720,7 +545,7 @@ class CreateView extends GetView<CreateController> {
controller.selectedIndexes.map((index) {
final file = rr.res.files[index];
return createTask(CreateTask(
req: file.req!..proxy = parseProxy(),
req: file.req!,
opt: Options(
name: file.name,
path: path.join(_pathController.text,
@@ -1,20 +1,16 @@
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';
@@ -38,22 +34,13 @@ class SettingView extends GetView<SettingController> {
final startCfg = appController.startConfig;
Timer? timer;
Future<bool> debounceSave(
{Future<String> Function()? check, bool needRestart = false}) {
var completer = Completer<bool>();
debounceSave({bool needRestart = false}) {
var completer = Completer<void>();
timer?.cancel();
timer = Timer(const Duration(milliseconds: 1000), () async {
if (check != null) {
final checkResult = await check();
if (checkResult.isNotEmpty) {
showErrorMessage(checkResult);
completer.complete(false);
return;
}
}
timer = Timer(const Duration(milliseconds: 1000), () {
appController
.saveConfig()
.then((_) => completer.complete(true))
.then(completer.complete)
.onError(completer.completeError);
if (needRestart) {
showMessage('tip'.tr, 'effectAfterRestart'.tr);
@@ -80,7 +67,6 @@ class SettingView extends GetView<SettingController> {
return DirectorySelector(
controller: downloadDirController,
showLabel: false,
showAndoirdToggle: true,
);
});
final buildMaxRunning = _buildConfigItem(
@@ -109,82 +95,16 @@ 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 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));
subtitle: const OpenInNew(
text: "Chrome",
url:
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
));
}
// 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 =
@@ -357,69 +277,6 @@ 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(
@@ -765,41 +622,11 @@ 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;
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;
});
}
await debounceSave(needRestart: true);
}
}
@@ -865,32 +692,6 @@ 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: () {
@@ -925,9 +726,7 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([
buildDownloadDir(),
buildMaxRunning(),
buildDefaultDirectDownload(),
buildBrowserExtension(),
buildAutoStartup(),
buildBrowserExtension()
]),
)),
const Text('HTTP'),
@@ -946,7 +745,6 @@ class SettingView extends GetView<SettingController> {
buildBtListenPort(),
buildBtTrackerSubscribeUrls(),
buildBtTrackers(),
buildBtSeedConfig(),
]),
)),
Text('ui'.tr),
@@ -1003,13 +801,6 @@ class SettingView extends GetView<SettingController> {
: null,
]),
)),
Text('developer'.tr),
Card(
child: Column(
children: _addDivider([
buildLogsDir(),
]),
)),
]),
))
],
@@ -1,10 +1,5 @@
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.open_in_new),
const Icon(Icons.play_circle),
onPressed: () {
launchUrl(Uri.parse(accessUrl),
webOnlyWindowName:
@@ -104,9 +104,14 @@ class TaskFilesView extends GetView<TaskFilesController> {
}()
: [
IconButton(
icon: const Icon(Icons.open_in_new),
icon: const Icon(Icons.play_circle),
onPressed: () async {
await OpenFilex.open(filePath);
print(filePath);
try {
await OpenFilex.open(filePath);
} catch (e) {
print(e);
}
}),
IconButton(
icon: const Icon(Icons.share),
@@ -1,13 +1,6 @@
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';
@@ -19,12 +12,9 @@ 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(
@@ -64,87 +54,7 @@ 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,12 +57,18 @@ class BuildTaskListView extends GetView {
return task.status == Status.running;
}
bool isFolderTask() {
return task.isFolder;
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)));
}
}
Future<void> showDeleteDialog(String id) {
final appController = Get.find<AppController>();
final keep = true.obs;
final context = Get.context!;
@@ -72,14 +78,11 @@ class BuildTaskListView extends GetView {
builder: (_) => AlertDialog(
title: Text('deleteTask'.tr),
content: Obx(() => CheckboxListTile(
value: appController
.downloaderConfig.value.extra.lastDeleteTaskKeep,
value: keep.value,
title: Text('deleteTaskTip'.tr,
style: context.textTheme.bodyLarge),
onChanged: (v) {
appController.downloaderConfig.update((val) {
val!.extra.lastDeleteTaskKeep = v!;
});
keep.value = v!;
})),
actions: [
TextButton(
@@ -93,10 +96,7 @@ class BuildTaskListView extends GetView {
),
onPressed: () async {
try {
final force = !appController
.downloaderConfig.value.extra.lastDeleteTaskKeep;
await appController.saveConfig();
await deleteTask(id, force);
await deleteTask(id, !keep.value);
Get.back();
} catch (e) {
showErrorMessage(e);
@@ -113,7 +113,12 @@ class BuildTaskListView extends GetView {
list.add(IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () {
task.explorer();
if (Util.isDesktop()) {
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
} else {
Get.rootDelegate
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
}
},
));
} else {
@@ -167,26 +172,18 @@ class BuildTaskListView extends GetView {
(total > 0 ? " / ${Util.fmtByte(total)}" : "");
}
final taskController = Get.find<TaskController>();
return Card(
elevation: 4.0,
child: InkWell(
onTap: () {
taskController.scaffoldKey.currentState?.openEndDrawer();
taskController.selectTask.value = task;
},
onDoubleTap: () {
task.open();
},
onTap: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(task.name),
leading: isFolderTask()
title: Text(fileName(task.meta)),
leading: (task.meta.res?.name.isNotEmpty ?? false
? const Icon(FaIcons.folder)
: Icon(FaIcons.allIcons[findIcon(task.name)])),
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))]))),
Row(
children: [
Expanded(
@@ -217,4 +214,29 @@ 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;
}
}
@@ -1,66 +0,0 @@
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,
),
],
),
);
}
}
-44
View File
@@ -1,44 +0,0 @@
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,30 +1,16 @@
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,
this.showAndoirdToggle = false})
{Key? key, required this.controller, this.showLabel = true})
: super(key: key);
@override
@@ -34,82 +20,6 @@ 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(
@@ -125,7 +35,17 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
},
)),
buildSelectWidget()
!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
].where((e) => e != null).map((e) => e!).toList(),
);
}
@@ -11,7 +11,7 @@ StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
..address = json['address'] as String
..storage = json['storage'] as String
..storageDir = json['storageDir'] as String
..refreshInterval = json['refreshInterval'] as int
..refreshInterval = (json['refreshInterval'] as num).toInt()
..apiToken = json['apiToken'] as String;
Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import '../../util/util.dart';
import '../common/libgopeed_channel.dart';
@@ -16,19 +17,7 @@ class LibgopeedBootNative implements LibgopeedBoot {
late LibgopeedInterface _libgopeed;
LibgopeedBootNative() {
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 {
if (!Util.isDesktop()) {
_libgopeed = LibgopeedChannel();
}
}
@@ -38,7 +27,8 @@ class LibgopeedBootNative implements LibgopeedBoot {
cfg.storage = 'bolt';
cfg.storageDir = Util.getStorageDir();
cfg.refreshInterval = 0;
var port = await _libgopeed.start(cfg);
var port =
Util.isDesktop() ? await _ffiStart(cfg) : await _libgopeed.start(cfg);
return port;
}
@@ -46,4 +36,26 @@ 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);
});
}
}
+9
View File
@@ -54,6 +54,15 @@ 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();
-15
View File
@@ -17,7 +17,6 @@ const enUS = {
'create': 'Create Task',
'directDownload': 'Direct Download',
'advancedOptions': 'Advanced Options',
'followSettings': 'Follow Settings',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link',
'downloadLinkHit':
@@ -36,7 +35,6 @@ 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':
@@ -60,11 +58,7 @@ 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',
@@ -98,14 +92,5 @@ 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',
},
};
-107
View File
@@ -1,107 +0,0 @@
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',
},
};
-96
View File
@@ -1,96 +0,0 @@
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',
},
};
-99
View File
@@ -1,99 +0,0 @@
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',
},
};
-100
View File
@@ -1,100 +0,0 @@
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",
},
};
-98
View File
@@ -1,98 +0,0 @@
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',
},
};
-99
View File
@@ -1,99 +0,0 @@
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': 'தொடக்கத்தின் போது இயங்கு',
},
};
+6 -10
View File
@@ -21,7 +21,8 @@ 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',
@@ -43,7 +44,8 @@ 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',
@@ -56,11 +58,9 @@ const trTR = {
'protocol': 'Protokol',
'port': 'Bağlantı noktası',
'apiToken': 'API anahtarı',
'notSet': 'AYARLI DEĞİL',
'set': 'AYARLI',
'portInUse': 'Bağlantı noktası [@port] kullanımda, lütfen bağlantı noktasını değiştirin',
'notSet': 'AYARLI',
'set': 'AYARLI DEĞİL',
'effectAfterRestart': 'Yeniden başlatma sonrası etki edecektir',
'show': 'Göster',
'startAll': 'Hepsini başlat',
'pauseAll': 'Hepsini durdur',
'deleteTask': 'Görevleri sil',
@@ -84,9 +84,6 @@ 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',
@@ -94,6 +91,5 @@ 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',
},
};
-15
View File
@@ -17,7 +17,6 @@ const zhCN = {
'create': '创建任务',
'directDownload': '直接下载',
'advancedOptions': '高级选项',
'followSettings': '跟随设置',
'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接',
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
@@ -35,7 +34,6 @@ const zhCN = {
'connections': '连接数',
'useServerCtime': '创建文件使用服务器时间',
'maxRunning': '最大下载数',
'defaultDirectDownload': '默认勾选直接下载',
'items': '@count 项',
'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试',
@@ -58,11 +56,7 @@ const zhCN = {
'apiToken': '接口令牌',
'notSet': '未设置',
'set': '已设置',
'portInUse': '端口[@port]已被占用,请更换端口',
'effectAfterRestart': '此配置项将在重启应用后生效',
'developer': '开发者',
'logDirectory': '日志目录',
'show': '显示',
'startAll': '全部开始',
'pauseAll': '全部暂停',
'deleteTask': '删除任务',
@@ -95,14 +89,5 @@ const zhCN = {
'thanks': '鸣谢',
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
'browserExtension': '浏览器扩展',
'launchAtStartup': '开机自动运行',
'seedConfig': '做种设置',
'seedKeep': '持续做种直到手动停止',
'seedRatio': '做种分享率',
'seedTime': '做种时间(分钟)',
'taskDetail': '任务详情',
'taskName': '任务名称',
'taskUrl': '任务链接',
'downloadPath': '下载路径',
}
};
+50 -65
View File
@@ -1,108 +1,93 @@
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': ',或直接拖曳種子檔到此處',
'downloadLinkHitDesktop': ',也可以通過拖拽Torrent檔至此以新增任務',
'download': '下載',
'noFileSelected': '請至少選擇一個檔案以繼續。',
'noStoragePermission': '需要儲存空間權限',
'noFileSelected': '沒有選取的檔案',
'noStoragePermission': '沒有存儲權',
'selectFile': '選擇檔案',
'rename': '重新命名',
'basic': '基本',
'rename': '重命名',
'basic': '一般',
'advanced': '進階',
'general': '一般',
'general': '通用設定',
'downloadDir': '下載目錄',
'downloadDirValid': '請選擇下載目錄',
'connections': '連接數',
'connections': '同時連接數',
'useServerCtime': '使用伺服器時間作為檔案建立時間',
'maxRunning': '最大執行任務數',
'defaultDirectDownload': '預設勾選直接下載',
'items': '@count 個項目',
'subscribeTracker': '訂閱 Tracker',
'subscribeFail': '訂閱失敗,請檢查網路或稍後再試',
'maxRunning': '最大同時下載數',
'items': '@count 項',
'subscribeTracker': '已訂閱的追蹤者列表',
'subscribeFail': '訂閱失敗',
'update': '更新',
'updateDaily': '每日更新',
'lastUpdate': '最後更新時間:@time',
'addTracker': '新增 Tracker',
'addTrackerHit': '請輸入 Tracker 伺服器 URL,每行一個',
'updateDaily': '每天自動更新',
'lastUpdate': '上次更新時間: @time',
'addTracker': '增加新的追蹤者',
'addTrackerHit': '請輸入追蹤者伺服器地址,每行一條',
'ui': '介面',
'theme': '主題',
'themeSystem': '系統',
'themeLight': '淺色',
'themeDark': '深色',
'themeSystem': '跟隨系統',
'themeLight': '亮色主題',
'themeDark': '深色主題',
'locale': '語言',
'about': '關於',
'homepage': '首頁',
'version': '版本',
'protocol': '協議',
'port': '連接埠',
'apiToken': 'API Token',
'homepage': '訪問主頁',
'version': '版本資訊',
'protocol': '通信協定',
'port': '埠',
'apiToken': 'API令牌',
'notSet': '未設定',
'set': '已設定',
'portInUse': '連接埠 [@port] 已被使用,請更改連接埠',
'effectAfterRestart': '重新啟動後生效',
'developer': '開發者',
'logDirectory': '日誌目錄',
'show': '顯示',
'effectAfterRestart': '重啟後生效',
'startAll': '全部開始',
'pauseAll': '全部暫停',
'deleteTask': '刪除任務',
'deleteTaskTip': '保留已下載的檔案',
'deleteTaskTip': '保留已經下載的檔案',
'delete': '刪除',
'newVersionTitle': '發現新版本 @version',
'newVersionUpdate': '立即更新',
'newVersionLater': '稍後',
'extensions': '擴充功能',
'extensionInstallUrl': '安裝 URL',
'newVersionTitle': '新版本 @version 已發佈',
'newVersionUpdate': '下載',
'newVersionLater': '取消',
'extensions': '擴充程式',
'extensionInstallUrl': '安裝連結',
'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新成功',
'extensionDelete': '刪除擴充功能',
'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '尋找擴充功能',
'extensionDevelop': '開發擴充功能',
'extensionFind': '取得擴充程式',
'extensionDevelop': '開發擴充程式',
'history': '歷史記錄',
'clearHistory': '清除歷史記錄',
'noHistoryFound': '未找到歷史記錄',
'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄',
'serviceTitle': '下載服務',
'serviceText': '運行中',
'serviceText': '執行中',
'network': '網路',
'proxy': 'Proxy',
'noProxy': '沒有 Proxy',
'systemProxy': '系統 Proxy',
'customProxy': '自訂 Proxy',
'proxy': '代理',
'noProxy': '不使用代理',
'systemProxy': '系統代理',
'customProxy': '自定義代理',
'server': '伺服器',
'username': '使用者名稱',
'username': '名稱',
'password': '密碼',
'thanks': '感謝',
'thanksDesc': '感謝所有幫助建立和發展 Gopeed 社群的貢獻者!',
'browserExtension': '瀏覽器擴充功能',
'launchAtStartup': '開機自動執行',
'seedConfig': '做種設定',
'seedKeep': '持續做種直到手動停止',
'seedRatio': '做種分享率',
'seedTime': '做種時間(分鐘)',
'taskDetail': '任務詳情',
'taskName': '任務名稱',
'taskUrl': '任務連結',
'downloadPath': '下載路徑',
'thanksDesc': '感謝所有為 Gopeed 社區建設添磚加瓦的貢獻者們!',
'browserExtension': '瀏覽器擴充程式',
}
};
+5 -17
View File
@@ -1,19 +1,13 @@
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/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';
import 'langs/vi_vn.dart';
import 'langs/tr_tr.dart';
final messages = _Messages();
@@ -28,12 +22,6 @@ class _Messages extends Translations {
...faIR,
...jaJP,
...viVN,
...taTA,
...trTR,
...plPL,
...itIT,
...idID,
...frFR,
...esES,
};
}
+30 -30
View File
@@ -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,6 +9,7 @@ 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';
@@ -16,28 +17,14 @@ import 'util/mac_secure_util.dart';
import 'util/package_info.dart';
import 'util/util.dart';
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);
void main() async {
await init();
onStart();
runApp(const AppView());
}
Future<void> init(Args args) async {
Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized();
await Util.initStorageDir();
await Database.instance.init();
@@ -50,10 +37,8 @@ Future<void> init(Args args) async {
skipTaskbar: false,
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
if (!args.hidden) {
await windowManager.show();
await windowManager.focus();
}
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
@@ -65,18 +50,27 @@ Future<void> init(Args args) 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;
controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg);
api.init(startCfg.network, controller.runningAddress(), startCfg.apiToken);
// 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);
}
} catch (e) {
logger.e("libgopeed init fail", e);
}
@@ -87,6 +81,12 @@ Future<void> init(Args args) 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 {
+11 -3
View File
@@ -1,7 +1,5 @@
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)) {
@@ -12,7 +10,17 @@ class FileExplorer {
}
static Future<void> _openDirectory(String directoryPath) async {
await launchUrlString("file://$directoryPath");
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]);
}
}
}
static Future<void> _openFile(String filePath) async {
+2 -6
View File
@@ -15,15 +15,11 @@ 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 = logsDir();
var logDir = Directory(logsDir());
final logDirPath = path.join(Util.getStorageDir(), 'logs');
var logDir = Directory(logDirPath);
if (!logDir.existsSync()) {
logDir.createSync();
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more