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

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
@@ -172,7 +174,7 @@ command:
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||

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

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

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

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
@@ -108,7 +110,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
|
||||
### 開發環境
|
||||
|
||||
1. Golang 1.21+
|
||||
1. Golang 1.22+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### 克隆項目
|
||||
@@ -168,7 +170,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+36
-11
@@ -8,7 +8,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
//go:embed banner.txt
|
||||
@@ -25,23 +27,46 @@ func Start(cfg *model.StartConfig) {
|
||||
panic(err)
|
||||
}
|
||||
if downloadCfg.FirstLoad {
|
||||
// Set default download config
|
||||
if cfg.DownloadConfig != nil {
|
||||
cfg.DownloadConfig.Merge(downloadCfg)
|
||||
// TODO Use PatchConfig
|
||||
rest.Downloader.PutConfig(cfg.DownloadConfig)
|
||||
downloadCfg = cfg.DownloadConfig
|
||||
}
|
||||
|
||||
downloadDir := downloadCfg.DownloadDir
|
||||
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
|
||||
var downloadDir string
|
||||
if base.InDocker == "true" {
|
||||
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||
} else {
|
||||
userDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
downloadDir = filepath.Join(userDir, "Downloads")
|
||||
if downloadDir == "" {
|
||||
if base.InDocker == "true" {
|
||||
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||
} else {
|
||||
userDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
downloadDir = filepath.Join(userDir, "Downloads")
|
||||
}
|
||||
}
|
||||
if downloadDir != "" {
|
||||
downloadCfg.DownloadDir = downloadDir
|
||||
rest.Downloader.PutConfig(downloadCfg)
|
||||
}
|
||||
}
|
||||
if downloadDir != "" {
|
||||
downloadCfg.DownloadDir = downloadDir
|
||||
rest.Downloader.PutConfig(downloadCfg)
|
||||
}
|
||||
}
|
||||
watchExit()
|
||||
|
||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func watchExit() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
fmt.Printf("Server is shutting down due to signal: %s\n", sig)
|
||||
rest.Downloader.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
+6
-2
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@@ -15,6 +16,8 @@ type args struct {
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||
|
||||
configPath *string
|
||||
}
|
||||
@@ -25,7 +28,7 @@ func parse() *args {
|
||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
|
||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
flag.Parse()
|
||||
@@ -57,8 +60,9 @@ func loadConfig(path string) *args {
|
||||
var args args
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
path = filepath.Join(dir, path)
|
||||
|
||||
@@ -48,6 +48,7 @@ func main() {
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: filepath.Join(dir, "storage"),
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
chown -R ${PUID}:${PGID} /app
|
||||
|
||||
|
||||
@@ -1,54 +1,58 @@
|
||||
module github.com/GopeedLab/gopeed
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
||||
github.com/anacrolix/torrent v1.53.3
|
||||
github.com/anacrolix/torrent v1.56.1
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
||||
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe
|
||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||
github.com/go-git/go-git/v5 v5.8.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/mattn/go-ieproxy v0.0.12
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||
golang.org/x/sync v0.6.0
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.7.0 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||
github.com/anacrolix/chansync v0.4.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
||||
github.com/anacrolix/chansync v0.5.1 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.21.1 // indirect
|
||||
github.com/anacrolix/envpprof v1.3.0 // indirect
|
||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
|
||||
github.com/anacrolix/generics v0.0.2 // indirect
|
||||
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 // indirect
|
||||
github.com/anacrolix/log v0.15.2 // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
||||
github.com/anacrolix/stm v0.5.0 // indirect
|
||||
github.com/anacrolix/sync v0.5.1 // indirect
|
||||
github.com/anacrolix/upnp v0.1.3 // indirect
|
||||
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||
github.com/anacrolix/utp v0.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
@@ -56,62 +60,69 @@ require (
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.5.3 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||
github.com/pion/ice/v2 v2.3.12 // indirect
|
||||
github.com/pion/interceptor v0.1.25 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pion/datachannel v1.5.8 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/ice/v2 v2.3.31 // indirect
|
||||
github.com/pion/interceptor v0.1.29 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.9 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.13 // indirect
|
||||
github.com/pion/rtp v1.8.3 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.7 // indirect
|
||||
github.com/pion/sctp v1.8.19 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.20 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
||||
github.com/pion/transport/v2 v2.2.9 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/webrtc/v3 v3.2.50 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tidwall/btree v1.7.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.3 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/mod v0.19.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
golang.org/x/tools v0.23.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.40.6 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
modernc.org/libc v1.55.6 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
zombiezen.com/go/sqlite v1.1.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.31.1 // indirect
|
||||
zombiezen.com/go/sqlite v1.3.0 // indirect
|
||||
)
|
||||
@@ -1,14 +1,16 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
GetConfig func(v any) bool
|
||||
ProxyUrl *url.URL
|
||||
GetConfig func(v any)
|
||||
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
||||
FileController
|
||||
//ContextDialer() (proxy.Dialer, error)
|
||||
}
|
||||
@@ -22,10 +24,9 @@ type DefaultFileController struct {
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
GetConfig: func(v any) {},
|
||||
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
||||
FileController: &DefaultFileController{},
|
||||
GetConfig: func(v any) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Fetcher defines the interface for a download protocol.
|
||||
// One fetcher for each download task
|
||||
// Each download task will have a corresponding Fetcher instance for the management of the download task
|
||||
type Fetcher interface {
|
||||
// Name return the name of the protocol.
|
||||
Name() string
|
||||
|
||||
Setup(ctl *controller.Controller)
|
||||
// Resolve resource info from request
|
||||
Resolve(req *base.Request) error
|
||||
@@ -31,6 +29,12 @@ type Fetcher interface {
|
||||
Wait() error
|
||||
}
|
||||
|
||||
type Uploader interface {
|
||||
Upload() error
|
||||
UploadedBytes() int64
|
||||
WaitUpload() error
|
||||
}
|
||||
|
||||
// FetcherMeta defines the meta information of a fetcher.
|
||||
type FetcherMeta struct {
|
||||
Req *base.Request `json:"req"`
|
||||
@@ -68,17 +72,57 @@ func (m *FetcherMeta) RootDirPath() string {
|
||||
}
|
||||
}
|
||||
|
||||
// FetcherBuilder defines the interface for a fetcher builder.
|
||||
type FetcherBuilder interface {
|
||||
// Schemes returns the schemes supported by the fetcher.
|
||||
Schemes() []string
|
||||
type FilterType int
|
||||
|
||||
const (
|
||||
// FilterTypeUrl url type, pattern is the scheme, e.g. http://github.com -> http
|
||||
FilterTypeUrl FilterType = iota
|
||||
// FilterTypeFile file type, pattern is the file extension name, e.g. test.torrent -> torrent
|
||||
FilterTypeFile
|
||||
// FilterTypeBase64 base64 data type, pattern is the data mime type, e.g. data:application/x-bittorrent;base64 -> application/x-bittorrent
|
||||
FilterTypeBase64
|
||||
)
|
||||
|
||||
type SchemeFilter struct {
|
||||
Type FilterType
|
||||
Pattern string
|
||||
}
|
||||
|
||||
func (s *SchemeFilter) Match(uri string) bool {
|
||||
uriUpper := strings.ToUpper(uri)
|
||||
patternUpper := strings.ToUpper(s.Pattern)
|
||||
switch s.Type {
|
||||
case FilterTypeUrl:
|
||||
return strings.HasPrefix(uriUpper, patternUpper+":")
|
||||
case FilterTypeFile:
|
||||
return strings.HasSuffix(uriUpper, "."+patternUpper)
|
||||
case FilterTypeBase64:
|
||||
return strings.HasPrefix(uriUpper, "DATA:"+patternUpper+";BASE64,")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FetcherManager manage and control the fetcher
|
||||
type FetcherManager interface {
|
||||
// Name return the name of the protocol.
|
||||
Name() string
|
||||
// Filters registers the supported schemes.
|
||||
Filters() []*SchemeFilter
|
||||
// Build returns a new fetcher.
|
||||
Build() Fetcher
|
||||
// ParseName name displayed when the task is not yet resolved, parsed from the request URL
|
||||
ParseName(u string) string
|
||||
// AutoRename returns whether the fetcher need renaming the download file when has the same name file.
|
||||
AutoRename() bool
|
||||
|
||||
// DefaultConfig returns the default configuration of the protocol.
|
||||
DefaultConfig() any
|
||||
// Store fetcher
|
||||
Store(fetcher Fetcher) (any, error)
|
||||
// Restore fetcher
|
||||
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
||||
// Close the fetcher manager, release resources.
|
||||
Close() error
|
||||
}
|
||||
|
||||
type DefaultFetcher struct {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package fetcher
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSchemeFilter_Match(t *testing.T) {
|
||||
type fields struct {
|
||||
Type FilterType
|
||||
Pattern string
|
||||
}
|
||||
type args struct {
|
||||
uri string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "url match",
|
||||
fields: fields{
|
||||
Type: FilterTypeUrl,
|
||||
Pattern: "https",
|
||||
},
|
||||
args: args{
|
||||
uri: "https://github.com",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "url not match",
|
||||
fields: fields{
|
||||
Type: FilterTypeUrl,
|
||||
Pattern: "https",
|
||||
},
|
||||
args: args{
|
||||
uri: "ftp://github.com",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "file match",
|
||||
fields: fields{
|
||||
Type: FilterTypeFile,
|
||||
Pattern: "torrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "d:/temp/test.torrent",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "file not match",
|
||||
fields: fields{
|
||||
Type: FilterTypeFile,
|
||||
Pattern: "torrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "d:/temp/test.txt",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "base64 match",
|
||||
fields: fields{
|
||||
Type: FilterTypeBase64,
|
||||
Pattern: "application/x-bittorrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "data:application/x-bittorrent;base64,xxx",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "base64 not match",
|
||||
fields: fields{
|
||||
Type: FilterTypeBase64,
|
||||
Pattern: "application/x-bittorrent",
|
||||
},
|
||||
args: args{
|
||||
uri: "data:application/javascript;base64,xxx",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SchemeFilter{
|
||||
Type: tt.fields.Type,
|
||||
Pattern: tt.fields.Pattern,
|
||||
}
|
||||
if got := s.Match(tt.args.uri); got != tt.want {
|
||||
t.Errorf("Match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,10 @@ package bt
|
||||
type config struct {
|
||||
ListenPort int `json:"listenPort"`
|
||||
Trackers []string `json:"trackers"`
|
||||
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
||||
SeedKeep bool `json:"seedKeep`
|
||||
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||
SeedRatio float64 `json:"seedRatio"`
|
||||
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||
SeedTime int64 `json:"seedTime"`
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package bt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/dnscache"
|
||||
)
|
||||
|
||||
// DnsCacheResolver resolves DNS requests for an HTTP client using an in-memory cache.
|
||||
type DnsCacheResolver struct {
|
||||
RefreshTimeout time.Duration
|
||||
|
||||
resolver dnscache.Resolver
|
||||
}
|
||||
|
||||
func (r *DnsCacheResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips, err := r.resolver.LookupHost(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var conn net.Conn
|
||||
for _, ip := range ips {
|
||||
var dialer net.Dialer
|
||||
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip, port))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (r *DnsCacheResolver) Run(ctx context.Context) {
|
||||
ticker := time.NewTicker(r.RefreshTimeout)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
r.resolver.Refresh(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
+284
-90
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -35,7 +35,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
||||
}
|
||||
|
||||
want := &base.Resource{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
@@ -60,61 +59,162 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||
defer proxyListener.Close()
|
||||
|
||||
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
|
||||
doResolve(t, buildConfigFetcher(&base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
System: false,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
}))
|
||||
}
|
||||
|
||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||
Extra: bt.ReqExtra{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
t.Run("Resolve Single File", func(t *testing.T) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||
Extra: bt.ReqExtra{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
want := &base.Resource{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
want := &base.Resource{
|
||||
Size: 1466714112,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||
Size: 1466714112,
|
||||
},
|
||||
},
|
||||
},
|
||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||
}
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Resolve Multi Files", func(t *testing.T) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/test.torrent",
|
||||
Extra: bt.ReqExtra{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
want := &base.Resource{
|
||||
Name: "test",
|
||||
Size: 107484864,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: "c.txt",
|
||||
Path: "path",
|
||||
Size: 98501754,
|
||||
},
|
||||
{
|
||||
Name: "b.txt",
|
||||
Size: 8904996,
|
||||
},
|
||||
{
|
||||
Name: "a.txt",
|
||||
Size: 78114,
|
||||
},
|
||||
},
|
||||
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
||||
}
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestFetcherManager_ParseName(t *testing.T) {
|
||||
type args struct {
|
||||
u string
|
||||
}
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "broken url",
|
||||
args: args{
|
||||
u: "magnet://!@#%github.com",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "dn",
|
||||
args: args{
|
||||
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed&dn=ubuntu-22.04-live-server-amd64.iso",
|
||||
},
|
||||
want: "ubuntu-22.04-live-server-amd64.iso",
|
||||
},
|
||||
{
|
||||
name: "no dn",
|
||||
args: args{
|
||||
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||
},
|
||||
want: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||
},
|
||||
{
|
||||
name: "non standard magnet",
|
||||
args: args{
|
||||
u: "magnet:?xxt=abcd",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fm := &FetcherManager{}
|
||||
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildFetcher() fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher.Setup(controller.NewController())
|
||||
fb := new(FetcherManager)
|
||||
fetcher := fb.Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
}
|
||||
|
||||
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
|
||||
fetcher := new(FetcherManager).Build()
|
||||
newController := controller.NewController()
|
||||
mockCfg := config{
|
||||
Trackers: []string{
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
}}
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
||||
}
|
||||
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
return proxyConfig.ToHandler()
|
||||
}
|
||||
newController.ProxyUrl = proxyUrl
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -5,12 +5,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/xiaoqidun/setft"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
@@ -21,6 +15,13 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/xiaoqidun/setft"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type RequestError struct {
|
||||
@@ -64,23 +65,13 @@ type Fetcher struct {
|
||||
eg *errgroup.Group
|
||||
}
|
||||
|
||||
func (f *Fetcher) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
f.ctl = ctl
|
||||
f.doneCh = make(chan error, 1)
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
exist := f.ctl.GetConfig(&f.config)
|
||||
if !exist {
|
||||
f.config = &config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
Connections: 1,
|
||||
}
|
||||
}
|
||||
f.ctl.GetConfig(&f.config)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,6 +79,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||
return err
|
||||
}
|
||||
f.meta.Req = req
|
||||
httpReq, err := f.buildRequest(nil, req)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -108,7 +100,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
}
|
||||
|
||||
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
||||
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
|
||||
// response 206 status code, support breakpoint continuation
|
||||
res.Range = true
|
||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||
@@ -120,7 +112,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
res.Size = parse
|
||||
}
|
||||
} else if base.HttpCodeOK == httpResp.StatusCode {
|
||||
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
||||
// response 200 status code, not support breakpoint continuation, get file size by Content-Length header
|
||||
// if not found, maybe chunked encoding
|
||||
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
||||
if contentLength != "" {
|
||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||
@@ -161,7 +154,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
file.Name = httpReq.URL.Hostname()
|
||||
}
|
||||
res.Files = append(res.Files, file)
|
||||
f.meta.Req = req
|
||||
f.meta.Res = res
|
||||
return nil
|
||||
}
|
||||
@@ -179,8 +171,12 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
||||
opts.Extra = &fhttp.OptsExtra{}
|
||||
}
|
||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||
if extra.Connections == 0 {
|
||||
if extra.Connections <= 0 {
|
||||
extra.Connections = f.config.Connections
|
||||
// Avoid zero connections configuration
|
||||
if extra.Connections <= 0 {
|
||||
extra.Connections = 1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -386,7 +382,8 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
method string
|
||||
body io.Reader
|
||||
)
|
||||
headers := make(map[string][]string)
|
||||
|
||||
headers := http.Header{}
|
||||
if req.Extra == nil {
|
||||
method = http.MethodGet
|
||||
} else {
|
||||
@@ -398,7 +395,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
}
|
||||
if len(extra.Header) > 0 {
|
||||
for k, v := range extra.Header {
|
||||
headers[k] = []string{v}
|
||||
headers.Set(k, v)
|
||||
}
|
||||
}
|
||||
if extra.Body != "" {
|
||||
@@ -406,8 +403,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
}
|
||||
}
|
||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||
// load user agent from config
|
||||
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
|
||||
headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
@@ -451,12 +447,11 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) buildClient() *http.Client {
|
||||
transport := &http.Transport{}
|
||||
transport := &http.Transport{
|
||||
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||
}
|
||||
// Cookie handle
|
||||
jar, _ := cookiejar.New(nil)
|
||||
if f.ctl.ProxyUrl != nil {
|
||||
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||
}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Jar: jar,
|
||||
@@ -467,30 +462,67 @@ type fetcherData struct {
|
||||
Chunks []*chunk
|
||||
}
|
||||
|
||||
type FetcherBuilder struct {
|
||||
type FetcherManager struct {
|
||||
}
|
||||
|
||||
var schemes = []string{"HTTP", "HTTPS"}
|
||||
|
||||
func (fb *FetcherBuilder) Schemes() []string {
|
||||
return schemes
|
||||
func (fm *FetcherManager) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
||||
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||
return []*fetcher.SchemeFilter{
|
||||
{
|
||||
Type: fetcher.FilterTypeUrl,
|
||||
Pattern: "HTTP",
|
||||
},
|
||||
{
|
||||
Type: fetcher.FilterTypeUrl,
|
||||
Pattern: "HTTPS",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
func (fm *FetcherManager) ParseName(u string) string {
|
||||
var name string
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Get filePath by URL
|
||||
name = path.Base(url.Path)
|
||||
// If file name is empty, use host name
|
||||
if name == "" || name == "/" || name == "." {
|
||||
name = url.Hostname()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) AutoRename() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) DefaultConfig() any {
|
||||
return &config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
Connections: 16,
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
_f := f.(*Fetcher)
|
||||
return &fetcherData{
|
||||
Chunks: _f.chunks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||
fd := v.(*fetcherData)
|
||||
fb := &FetcherBuilder{}
|
||||
fb := &FetcherManager{}
|
||||
fetcher := fb.Build().(*Fetcher)
|
||||
fetcher.meta = meta
|
||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||
@@ -501,3 +533,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
||||
return fetcher
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"net"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -193,6 +194,54 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetcherManager_ParseName(t *testing.T) {
|
||||
type args struct {
|
||||
u string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "broken url",
|
||||
args: args{
|
||||
u: "https://!@#%github.com",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "file path",
|
||||
args: args{
|
||||
u: "https://github.com/index.html",
|
||||
},
|
||||
want: "index.html",
|
||||
},
|
||||
{
|
||||
name: "file path with query and hash",
|
||||
args: args{
|
||||
u: "https://github.com/a/b/index.html/#list?name=1",
|
||||
},
|
||||
want: "index.html",
|
||||
},
|
||||
{
|
||||
name: "no file path",
|
||||
args: args{
|
||||
u: "https://github.com",
|
||||
},
|
||||
want: "github.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fm := &FetcherManager{}
|
||||
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||
}
|
||||
@@ -307,7 +356,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fb := new(FetcherBuilder)
|
||||
fb := new(FetcherManager)
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
data, err := fb.Store(fetcher)
|
||||
if err != nil {
|
||||
@@ -338,7 +387,13 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||
fetcher := downloadReady(httpListener, 4, t)
|
||||
ctl := controller.NewController()
|
||||
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
|
||||
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
return (&base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
}).ToHandler()
|
||||
}
|
||||
fetcher.Setup(ctl)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
@@ -356,19 +411,21 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
||||
}
|
||||
|
||||
func buildFetcher() *Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher.Setup(controller.NewController())
|
||||
fm := new(FetcherManager)
|
||||
fetcher := fm.Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(fm.DefaultConfig())), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher.(*Fetcher)
|
||||
}
|
||||
|
||||
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher := new(FetcherManager).Build()
|
||||
newController := controller.NewController()
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
newController.GetConfig = func(v any) {
|
||||
json.Unmarshal([]byte(test.ToJson(cfg)), v)
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
|
||||
@@ -2,12 +2,12 @@ package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/armon/go-socks5"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -24,6 +24,11 @@ const (
|
||||
Dir = "./"
|
||||
BuildFile = Dir + BuildName
|
||||
|
||||
ExternalDownloadUrl = "https://raw.githubusercontent.com/GopeedLab/gopeed/v1.5.6/_docs/img/banner.png"
|
||||
ExternalDownloadName = "banner.png"
|
||||
ExternalDownloadSize = 26416
|
||||
//ExternalDownloadMd5 = "c67c6e3cae79a95342485676571e8a5c"
|
||||
|
||||
DownloadName = "download.data"
|
||||
DownloadRename = "download (1).data"
|
||||
DownloadFile = Dir + DownloadName
|
||||
|
||||
+147
-6
@@ -3,7 +3,10 @@ package base
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/mattn/go-ieproxy"
|
||||
"golang.org/x/exp/slices"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,6 +16,8 @@ type Request struct {
|
||||
Extra any `json:"extra"`
|
||||
// Labels is used to mark the download task
|
||||
Labels map[string]string `json:"labels"`
|
||||
// Proxy is special proxy config for request
|
||||
Proxy *RequestProxy `json:"proxy"`
|
||||
}
|
||||
|
||||
func (r *Request) Validate() error {
|
||||
@@ -22,6 +27,37 @@ func (r *Request) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type RequestProxyMode string
|
||||
|
||||
const (
|
||||
// RequestProxyModeFollow follow setting proxy
|
||||
RequestProxyModeFollow RequestProxyMode = "follow"
|
||||
// RequestProxyModeNone not use proxy
|
||||
RequestProxyModeNone RequestProxyMode = "none"
|
||||
// RequestProxyModeCustom custom proxy
|
||||
RequestProxyModeCustom RequestProxyMode = "custom"
|
||||
)
|
||||
|
||||
type RequestProxy struct {
|
||||
Mode RequestProxyMode `json:"mode"`
|
||||
Scheme string `json:"scheme"`
|
||||
Host string `json:"host"`
|
||||
Usr string `json:"usr"`
|
||||
Pwd string `json:"pwd"`
|
||||
}
|
||||
|
||||
func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||
if p == nil || p.Mode != RequestProxyModeCustom {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.Scheme == "" || p.Host == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd))
|
||||
}
|
||||
|
||||
// Resource download resource
|
||||
type Resource struct {
|
||||
// if name is not empty, the resource is a folder and the name is the folder name
|
||||
@@ -91,12 +127,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
||||
}
|
||||
|
||||
func (o *Options) Clone() *Options {
|
||||
return &Options{
|
||||
Name: o.Name,
|
||||
Path: o.Path,
|
||||
SelectFiles: o.SelectFiles,
|
||||
Extra: o.Extra,
|
||||
}
|
||||
return util.DeepClone(o)
|
||||
}
|
||||
|
||||
func ParseReqExtra[E any](req *Request) error {
|
||||
@@ -128,3 +159,113 @@ func ParseOptsExtra[E any](opts *Options) error {
|
||||
opts.Extra = &t
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||
type DownloaderStoreConfig struct {
|
||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||
|
||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||
Extra map[string]any `json:"extra"`
|
||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.MaxRunning == 0 {
|
||||
cfg.MaxRunning = 5
|
||||
}
|
||||
if cfg.ProtocolConfig == nil {
|
||||
cfg.ProtocolConfig = make(map[string]any)
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *DownloaderStoreConfig {
|
||||
if beforeCfg == nil {
|
||||
return cfg
|
||||
}
|
||||
if cfg.DownloadDir == "" {
|
||||
cfg.DownloadDir = beforeCfg.DownloadDir
|
||||
}
|
||||
if cfg.MaxRunning == 0 {
|
||||
cfg.MaxRunning = beforeCfg.MaxRunning
|
||||
}
|
||||
if cfg.ProtocolConfig == nil {
|
||||
cfg.ProtocolConfig = beforeCfg.ProtocolConfig
|
||||
}
|
||||
if cfg.Extra == nil {
|
||||
cfg.Extra = beforeCfg.Extra
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = beforeCfg.Proxy
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
// System is the flag that use system proxy
|
||||
System bool `json:"system"`
|
||||
Scheme string `json:"scheme"`
|
||||
Host string `json:"host"`
|
||||
Usr string `json:"usr"`
|
||||
Pwd string `json:"pwd"`
|
||||
}
|
||||
|
||||
func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||
if cfg == nil || cfg.Enable == false {
|
||||
return nil
|
||||
}
|
||||
if cfg.System {
|
||||
ieproxy.ReloadConf()
|
||||
return ieproxy.GetProxyFunc()
|
||||
}
|
||||
if cfg.Scheme == "" || cfg.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return http.ProxyURL(util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd))
|
||||
}
|
||||
|
||||
// ToUrl returns the proxy url, just for git clone
|
||||
func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
||||
if cfg == nil || cfg.Enable == false {
|
||||
return nil
|
||||
}
|
||||
if cfg.System {
|
||||
ieproxy.ReloadConf()
|
||||
static := ieproxy.GetConf().Static
|
||||
if static.Active && len(static.Protocols) > 0 {
|
||||
// If only one protocol, use it
|
||||
if len(static.Protocols) == 1 {
|
||||
for _, v := range static.Protocols {
|
||||
return parseUrlSafe(v)
|
||||
}
|
||||
}
|
||||
// Check https
|
||||
if v, ok := static.Protocols["https"]; ok {
|
||||
return parseUrlSafe(v)
|
||||
}
|
||||
// Check http
|
||||
if v, ok := static.Protocols["http"]; ok {
|
||||
return parseUrlSafe(v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if cfg.Scheme == "" || cfg.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
|
||||
}
|
||||
|
||||
func parseUrlSafe(rawUrl string) *url.URL {
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fields *DownloaderStoreConfig
|
||||
want *DownloaderStoreConfig
|
||||
}{
|
||||
{
|
||||
"Init",
|
||||
&DownloaderStoreConfig{},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init MaxRunning",
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init ProtocolConfig",
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init Proxy",
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &DownloaderStoreConfig{
|
||||
FirstLoad: tt.fields.FirstLoad,
|
||||
DownloadDir: tt.fields.DownloadDir,
|
||||
MaxRunning: tt.fields.MaxRunning,
|
||||
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||
Extra: tt.fields.Extra,
|
||||
Proxy: tt.fields.Proxy,
|
||||
}
|
||||
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Init() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloaderStoreConfig_Merge(t *testing.T) {
|
||||
type args struct {
|
||||
beforeCfg *DownloaderStoreConfig
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields *DownloaderStoreConfig
|
||||
args args
|
||||
want *DownloaderStoreConfig
|
||||
}{
|
||||
{
|
||||
"Merge Nil",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: nil,
|
||||
},
|
||||
&DownloaderStoreConfig{},
|
||||
},
|
||||
{
|
||||
"Merge DownloadDir No Override",
|
||||
&DownloaderStoreConfig{
|
||||
DownloadDir: "before",
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
DownloadDir: "after",
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
DownloadDir: "before",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge DownloadDir Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
DownloadDir: "after",
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
DownloadDir: "after",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge MaxRunning No Override",
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 1,
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge MaxRunning Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge ProtocolConfig No Override",
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge ProtocolConfig Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Extra No Override",
|
||||
&DownloaderStoreConfig{
|
||||
Extra: map[string]any{},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Extra: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Extra: map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Extra Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Extra: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Extra: map[string]any{
|
||||
"key": "after",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Proxy No Override",
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Scheme: "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Proxy Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Scheme: "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Scheme: "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &DownloaderStoreConfig{
|
||||
FirstLoad: tt.fields.FirstLoad,
|
||||
DownloadDir: tt.fields.DownloadDir,
|
||||
MaxRunning: tt.fields.MaxRunning,
|
||||
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||
Extra: tt.fields.Extra,
|
||||
Proxy: tt.fields.Proxy,
|
||||
}
|
||||
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Merge() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+244
-123
File diff suppressed because it is too large.
Load diff
+167
-39
@@ -4,6 +4,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -83,39 +86,148 @@ func TestDownloader_Create(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||
// No proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return nil
|
||||
func TestDownloader_CreateDirectBatch(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
defer listener.Close()
|
||||
|
||||
downloader := NewDownloader(nil)
|
||||
if err := downloader.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
downloader.DeleteByStatues(nil, true)
|
||||
downloader.Clear()
|
||||
}()
|
||||
|
||||
reqs := make([]*base.Request, 0)
|
||||
fileNames := make([]string, 0)
|
||||
for i := 0; i < 5; i++ {
|
||||
req := &base.Request{
|
||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
reqs = append(reqs, req)
|
||||
if i == 0 {
|
||||
fileNames = append(fileNames, test.DownloadName)
|
||||
} else {
|
||||
arr := strings.Split(test.DownloadName, ".")
|
||||
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(reqs))
|
||||
downloader.Listener(func(event *Event) {
|
||||
if event.Key == EventKeyDone {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
// Disable proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Enable = false
|
||||
return proxyCfg
|
||||
})
|
||||
// Invalid proxy scheme
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Scheme = ""
|
||||
return proxyCfg
|
||||
})
|
||||
// Invalid proxy host
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Host = ""
|
||||
return proxyCfg
|
||||
})
|
||||
// Use proxy without auth
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
})
|
||||
// Use proxy with auth
|
||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
|
||||
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
||||
Path: test.Dir,
|
||||
Name: test.DownloadName,
|
||||
Extra: http.OptsExtra{
|
||||
Connections: 4,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
tasks := downloader.GetTasks()
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("CreateDirectBatch() task got = %v, want %v", len(tasks), len(reqs))
|
||||
}
|
||||
|
||||
nameMatchMap := make(map[string]bool)
|
||||
for _, name := range fileNames {
|
||||
if _, err := os.Stat(test.Dir + "/" + name); os.IsNotExist(err) {
|
||||
t.Errorf("CreateDirectBatch() file not exist: %v", name)
|
||||
}
|
||||
for _, task := range tasks {
|
||||
if name == task.Meta.Opts.Name {
|
||||
nameMatchMap[task.Meta.Opts.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(nameMatchMap) != len(reqs) {
|
||||
t.Errorf("CreateDirectBatch() names got = %v, want %v", len(nameMatchMap), len(reqs))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig) {
|
||||
httpListener := test.StartTestFileServer()
|
||||
defer httpListener.Close()
|
||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||
// No proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
return nil
|
||||
}, nil)
|
||||
// Disable proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.Enable = false
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Enable system proxy but not set proxy environment variable
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.System = true
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Enable proxy but error proxy environment variable
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
||||
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
||||
proxyCfg.System = true
|
||||
return proxyCfg
|
||||
}, func(err error) {
|
||||
if err == nil {
|
||||
t.Fatal("doTestDownloaderCreateWithProxy() got = nil, want error")
|
||||
}
|
||||
})
|
||||
// Enable system proxy and set proxy environment variable
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
||||
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
||||
proxyCfg.System = true
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Invalid proxy scheme
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.Scheme = ""
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Invalid proxy host
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
proxyCfg.Host = ""
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Use proxy without auth
|
||||
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
// Use proxy with auth
|
||||
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
}, nil)
|
||||
|
||||
// Request proxy mode follow
|
||||
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||
reqProxy.Mode = base.RequestProxyModeFollow
|
||||
return reqProxy
|
||||
}, nil, nil)
|
||||
|
||||
// Request proxy mode none
|
||||
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||
reqProxy.Mode = base.RequestProxyModeNone
|
||||
return reqProxy
|
||||
}, nil, nil)
|
||||
|
||||
// Request proxy mode custom
|
||||
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||
return reqProxy
|
||||
}, nil, nil)
|
||||
}
|
||||
|
||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
||||
usr, pwd := "", ""
|
||||
if auth {
|
||||
usr, pwd = "admin", "123"
|
||||
@@ -128,29 +240,45 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer downloader.Clear()
|
||||
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&DownloaderProxyConfig{
|
||||
globalProxyCfg := &base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
})
|
||||
}
|
||||
if buildProxyConfig != nil {
|
||||
globalProxyCfg = buildProxyConfig(globalProxyCfg)
|
||||
}
|
||||
downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg
|
||||
|
||||
req := &base.Request{
|
||||
URL: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
|
||||
URL: test.ExternalDownloadUrl,
|
||||
}
|
||||
if buildReqProxy != nil {
|
||||
req.Proxy = buildReqProxy(&base.RequestProxy{
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
})
|
||||
}
|
||||
rr, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if errHandler == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errHandler(err)
|
||||
return
|
||||
}
|
||||
want := &base.Resource{
|
||||
Size: test.BuildSize,
|
||||
Size: test.ExternalDownloadSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
Name: test.ExternalDownloadName,
|
||||
Path: "",
|
||||
Size: test.BuildSize,
|
||||
Size: test.ExternalDownloadSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -281,11 +409,11 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
||||
|
||||
var httpCfg map[string]any
|
||||
exits := downloader.getProtocolConfig("http", &httpCfg)
|
||||
if exits {
|
||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
||||
if !exits {
|
||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, true)
|
||||
}
|
||||
|
||||
storeCfg := &DownloaderStoreConfig{
|
||||
storeCfg := &base.DownloaderStoreConfig{
|
||||
DownloadDir: "./downloads",
|
||||
ProtocolConfig: map[string]any{
|
||||
"http": map[string]any{
|
||||
|
||||
@@ -3,6 +3,7 @@ package engine
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
gojaurl "github.com/dop251/goja_nodejs/url"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -106,11 +106,11 @@ func (e *Engine) await(value any) {
|
||||
}
|
||||
|
||||
func (e *Engine) Close() {
|
||||
e.loop.Stop()
|
||||
e.loop.StopNoWait()
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ProxyURL *url.URL
|
||||
ProxyConfig *base.DownloaderProxyConfig
|
||||
}
|
||||
|
||||
func NewEngine(cfg *Config) *Engine {
|
||||
@@ -135,7 +135,7 @@ func NewEngine(cfg *Config) *Engine {
|
||||
if err := formdata.Enable(runtime); err != nil {
|
||||
return
|
||||
}
|
||||
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
|
||||
if err := xhr.Enable(runtime, cfg.ProxyConfig.ToHandler()); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := runtime.RunString(polyfillScript); err != nil {
|
||||
@@ -145,6 +145,10 @@ func NewEngine(cfg *Config) *Engine {
|
||||
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
||||
return
|
||||
}
|
||||
// polyfill window
|
||||
if err := runtime.Set("window", runtime.GlobalObject()); err != nil {
|
||||
return
|
||||
}
|
||||
// polyfill window.location
|
||||
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
||||
return
|
||||
@@ -174,7 +178,15 @@ func resolveResult(value goja.Value) (any, error) {
|
||||
if err, ok := p.Result().Export().(error); ok {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, errors.New(p.Result().String())
|
||||
stack := p.Result().String()
|
||||
result := p.Result()
|
||||
if ro, ok := result.(*goja.Object); ok {
|
||||
stackVal := ro.Get("stack")
|
||||
if stackVal != nil && stackVal.String() != "" {
|
||||
stack = stackVal.String()
|
||||
}
|
||||
}
|
||||
return nil, errors.New(stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/dop251/goja"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -19,6 +13,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func TestPolyfill(t *testing.T) {
|
||||
@@ -26,6 +27,8 @@ func TestPolyfill(t *testing.T) {
|
||||
doTestPolyfill(t, "XMLHttpRequest")
|
||||
doTestPolyfill(t, "Blob")
|
||||
doTestPolyfill(t, "FormData")
|
||||
doTestPolyfill(t, "TextDecoder")
|
||||
doTestPolyfill(t, "TextEncoder")
|
||||
doTestPolyfill(t, "fetch")
|
||||
doTestPolyfill(t, "__gopeed_create_vm")
|
||||
}
|
||||
@@ -69,6 +72,44 @@ async function testOctetStream(file){
|
||||
return await resp.text();
|
||||
}
|
||||
|
||||
async function testRedirect() {
|
||||
const url = host + '/redirect?num=3'
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(url, {
|
||||
method: 'HEAD',
|
||||
redirect: 'error',
|
||||
}).then(()=>reject())
|
||||
|
||||
fetch(url, {
|
||||
method: 'HEAD',
|
||||
redirect: 'follow',
|
||||
}).then((res) =>res.headers.has('location') && reject()).catch(() => reject())
|
||||
|
||||
fetch(url, {
|
||||
method: 'HEAD',
|
||||
redirect: 'manual',
|
||||
}).then((res) => {
|
||||
const location = res.headers.get('location');
|
||||
location ? resolve(location) : reject()
|
||||
}).catch(() => reject())
|
||||
})
|
||||
}
|
||||
|
||||
async function testResponseUrl() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', host+'/redirect?num=3');
|
||||
xhr.onload = function(){
|
||||
if (xhr.responseURL.includes('/redirect?num=0')){
|
||||
resolve();
|
||||
}else{
|
||||
reject();
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
async function testFormData(file){
|
||||
const formData = new FormData();
|
||||
formData.append('name', 'test');
|
||||
@@ -80,6 +121,30 @@ async function testFormData(file){
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
function testHeader(){
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', host+'/header');
|
||||
xhr.setRequestHeader('X-Gopeed-Test', 'test1');
|
||||
xhr.setRequestHeader('x-gopeed-test', 'test2');
|
||||
xhr.setRequestHeader('x-Gopeed-test', 'test3');
|
||||
xhr.onload = function(){
|
||||
const testHeader1 = xhr.getResponseHeader("X-Gopeed-Test");
|
||||
const testHeader2 = xhr.getResponseHeader("x-gopeed-test");
|
||||
const testHeader3 = xhr.getResponseHeader("x-Gopeed-test");
|
||||
const expect = 'test1, test2, test3';
|
||||
const all = xhr.getAllResponseHeaders();
|
||||
if(testHeader1 === expect && testHeader2 === expect && testHeader3 === expect
|
||||
&& all.includes('X-Gopeed-Test: '+expect)){
|
||||
resolve();
|
||||
}else{
|
||||
reject();
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
function testProgress(){
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
@@ -172,6 +237,20 @@ function testTimeout(){
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("testRedirect", func(t *testing.T) {
|
||||
_, err := callTestFun(engine, "testRedirect")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("testResponseUrl", func(t *testing.T) {
|
||||
_, err = callTestFun(engine, "testResponseUrl")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
func() {
|
||||
jsFile, goFile, md5 := buildFile(t, engine.Runtime)
|
||||
result, err = callTestFun(engine, "testFormData", jsFile)
|
||||
@@ -190,14 +269,19 @@ function testTimeout(){
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = callTestFun(engine, "testHeader")
|
||||
if err != nil {
|
||||
t.Fatal("header test failed", err)
|
||||
}
|
||||
|
||||
_, err = callTestFun(engine, "testProgress")
|
||||
if err != nil {
|
||||
t.Fatal("progress test failed")
|
||||
t.Fatal("progress test failed", err)
|
||||
}
|
||||
|
||||
_, err = callTestFun(engine, "testAbort")
|
||||
if err != nil {
|
||||
t.Fatal("abort test failed")
|
||||
t.Fatal("abort test failed", err)
|
||||
}
|
||||
|
||||
_, err = callTestFun(engine, "testTimeout")
|
||||
@@ -217,8 +301,16 @@ func doTestFetchWithProxy(t *testing.T, usr, pwd string) {
|
||||
|
||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||
defer proxyListener.Close()
|
||||
|
||||
engine := NewEngine(&Config{ProxyURL: util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)})
|
||||
engine := NewEngine(&Config{
|
||||
ProxyConfig: &base.DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
System: false,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
},
|
||||
})
|
||||
|
||||
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -320,6 +412,15 @@ func startServer() net.Listener {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
mux.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
|
||||
for k, v := range r.Header {
|
||||
if strings.HasPrefix(k, "X-Gopeed") {
|
||||
w.Header().Set(k, strings.Join(v, ", "))
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
mux.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) {
|
||||
buf, _ := io.ReadAll(r.Body)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -359,6 +460,16 @@ func startServer() net.Listener {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
||||
num := r.URL.Query().Get("num")
|
||||
n, _ := strconv.Atoi(num)
|
||||
if n > 0 {
|
||||
http.Redirect(w, r, fmt.Sprintf("/redirect?num=%d", n-1), http.StatusFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
})
|
||||
server.Handler = mux
|
||||
go server.Serve(listener)
|
||||
return listener
|
||||
|
||||
@@ -2,16 +2,19 @@ package xhr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||
"github.com/dop251/goja"
|
||||
"errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,6 +26,12 @@ const (
|
||||
eventTimeout = "timeout"
|
||||
)
|
||||
|
||||
const (
|
||||
redirectError = "error"
|
||||
redirectFollow = "follow"
|
||||
redirectManual = "manual"
|
||||
)
|
||||
|
||||
type ProgressEvent struct {
|
||||
Type string `json:"type"`
|
||||
LengthComputable bool `json:"lengthComputable"`
|
||||
@@ -117,18 +126,26 @@ type XMLHttpRequestUpload struct {
|
||||
type XMLHttpRequest struct {
|
||||
method string
|
||||
url string
|
||||
requestHeaders map[string]string
|
||||
responseHeaders map[string]string
|
||||
requestHeaders http.Header
|
||||
responseHeaders http.Header
|
||||
aborted bool
|
||||
proxyUrl *url.URL
|
||||
proxyHandler func(r *http.Request) (*url.URL, error)
|
||||
|
||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||
Timeout int `json:"timeout"`
|
||||
ReadyState int `json:"readyState"`
|
||||
Status int `json:"status"`
|
||||
StatusText string `json:"statusText"`
|
||||
Response string `json:"response"`
|
||||
ResponseText string `json:"responseText"`
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||
Timeout int `json:"timeout"`
|
||||
ReadyState int `json:"readyState"`
|
||||
Status int `json:"status"`
|
||||
StatusText string `json:"statusText"`
|
||||
Response string `json:"response"`
|
||||
ResponseText string `json:"responseText"`
|
||||
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseURL
|
||||
// https://xhr.spec.whatwg.org/#the-responseurl-attribute
|
||||
ResponseUrl string `json:"responseURL"`
|
||||
// extend fetch redirect
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#redirect
|
||||
// https://fetch.spec.whatwg.org/#concept-request-redirect-mode
|
||||
Redirect string `json:"redirect"`
|
||||
*EventProp
|
||||
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
|
||||
}
|
||||
@@ -136,13 +153,13 @@ type XMLHttpRequest struct {
|
||||
func (xhr *XMLHttpRequest) Open(method, url string) {
|
||||
xhr.method = method
|
||||
xhr.url = url
|
||||
xhr.requestHeaders = make(map[string]string)
|
||||
xhr.responseHeaders = make(map[string]string)
|
||||
xhr.requestHeaders = make(http.Header)
|
||||
xhr.responseHeaders = make(http.Header)
|
||||
xhr.doReadystatechange(1)
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
||||
xhr.requestHeaders[key] = value
|
||||
xhr.requestHeaders.Add(key, value)
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
@@ -152,31 +169,33 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
var (
|
||||
contentType string
|
||||
contentLength int64
|
||||
isStringBody bool
|
||||
)
|
||||
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
||||
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
||||
} else {
|
||||
switch d.(type) {
|
||||
switch v := d.(type) {
|
||||
case string:
|
||||
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string)))
|
||||
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
|
||||
contentType = "text/plain;charset=UTF-8"
|
||||
contentLength = int64(len(d.(string)))
|
||||
contentLength = int64(len(v))
|
||||
isStringBody = true
|
||||
case *file.File:
|
||||
req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader)
|
||||
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
|
||||
contentType = "application/octet-stream"
|
||||
contentLength = d.(*file.File).Size
|
||||
contentLength = v.Size
|
||||
case *formdata.FormData:
|
||||
pr, pw := io.Pipe()
|
||||
mw := NewMultipart(pw)
|
||||
for _, e := range d.(*formdata.FormData).Entries() {
|
||||
for _, e := range v.Entries() {
|
||||
arr := e.([]any)
|
||||
k := arr[0].(string)
|
||||
v := arr[1]
|
||||
switch v.(type) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
mw.WriteField(k, v.(string))
|
||||
mw.WriteField(k, v)
|
||||
case *file.File:
|
||||
mw.WriteFile(k, v.(*file.File))
|
||||
mw.WriteFile(k, v)
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
@@ -193,27 +212,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
xhr.callOnerror()
|
||||
return
|
||||
}
|
||||
if contentType != "" {
|
||||
req.Header = xhr.requestHeaders
|
||||
// Only string body can specify Content-Type header by user
|
||||
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
if contentLength > 0 {
|
||||
req.ContentLength = contentLength
|
||||
}
|
||||
for k, v := range xhr.requestHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
transport := &http.Transport{}
|
||||
if xhr.proxyUrl != nil {
|
||||
transport.Proxy = http.ProxyURL(xhr.proxyUrl)
|
||||
transport := &http.Transport{
|
||||
Proxy: xhr.proxyHandler,
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if xhr.Redirect == redirectManual {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
if xhr.Redirect == redirectError {
|
||||
return errors.New("redirect failed")
|
||||
}
|
||||
if len(via) > 20 {
|
||||
return errors.New("too many redirects")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// handle timeout error
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
if xhr.Timeout > 0 {
|
||||
xhr.Upload.callOntimeout()
|
||||
xhr.callOntimeout()
|
||||
@@ -229,9 +259,12 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
if !xhr.aborted {
|
||||
xhr.Upload.callOnload()
|
||||
}
|
||||
for k, v := range resp.Header {
|
||||
xhr.responseHeaders[k] = v[0]
|
||||
}
|
||||
|
||||
responseUrl := resp.Request.URL
|
||||
responseUrl.Fragment = ""
|
||||
xhr.ResponseUrl = responseUrl.String()
|
||||
|
||||
xhr.responseHeaders = resp.Header
|
||||
xhr.Status = resp.StatusCode
|
||||
xhr.StatusText = resp.Status
|
||||
xhr.doReadystatechange(2)
|
||||
@@ -260,7 +293,7 @@ func (xhr *XMLHttpRequest) Abort() {
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
||||
return xhr.responseHeaders[key]
|
||||
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
||||
@@ -268,7 +301,7 @@ func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
|
||||
for k, v := range xhr.responseHeaders {
|
||||
buf.WriteString(k)
|
||||
buf.WriteString(": ")
|
||||
buf.WriteString(v)
|
||||
buf.WriteString(strings.Join(v, ", "))
|
||||
buf.WriteString("\r\n")
|
||||
}
|
||||
return buf.String()
|
||||
@@ -313,7 +346,7 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
|
||||
return data.String()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
||||
func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL, error)) error {
|
||||
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
if len(call.Arguments) < 1 {
|
||||
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
|
||||
@@ -327,7 +360,7 @@ func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
||||
})
|
||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
instance := &XMLHttpRequest{
|
||||
proxyUrl: proxyUrl,
|
||||
proxyHandler: proxyHandler,
|
||||
Upload: &XMLHttpRequestUpload{
|
||||
EventProp: &EventProp{
|
||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||
@@ -386,10 +419,9 @@ func (w *multipartWrapper) Size() int64 {
|
||||
w.statWriter.Close()
|
||||
size := int64(w.statBuffer.Len())
|
||||
for _, v := range w.fields {
|
||||
switch v.(type) {
|
||||
switch v := v.(type) {
|
||||
case *file.File:
|
||||
f := v.(*file.File)
|
||||
size += f.Size
|
||||
size += v.Size
|
||||
}
|
||||
}
|
||||
return size
|
||||
@@ -397,18 +429,17 @@ func (w *multipartWrapper) Size() int64 {
|
||||
|
||||
func (w *multipartWrapper) Send() error {
|
||||
for k, v := range w.fields {
|
||||
switch v.(type) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
if err := w.writer.WriteField(k, v.(string)); err != nil {
|
||||
if err := w.writer.WriteField(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
case *file.File:
|
||||
f := v.(*file.File)
|
||||
fw, err := w.writer.CreateFormFile(k, f.Name)
|
||||
fw, err := w.writer.CreateFormFile(k, v.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = io.Copy(fw, f); err != nil {
|
||||
if _, err = io.Copy(fw, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e<r;){var u=t.charCodeAt(e++);if(u>=55296&&u<=56319){if(e<r){var f=t.charCodeAt(e);56320==(64512&f)&&(++e,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o<e;){var n,i,a,s,u=t[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p<c;)h+=String.fromCharCode.apply(String,r.slice(p,p+=4096));return h};function b(){var e=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;e&&r&&(XMLHttpRequest.prototype.send=function(t){t instanceof Blob?(this.setRequestHeader("Content-Type",t.type),r.call(this,t)):r.call(this,t)});try{new File([],""),t.File=i.File,t.FileReader=i.FileReader}catch(e){try{t.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(e){t.File=function(t,e,r){var o=new Blob(t,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=e.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),t.Blob=f?i.Blob:p):l?(b(),t.Blob=h):function(){function e(t){for(var e=new Array(t.byteLength),r=new Uint8Array(t),o=e.length;o--;)e[o]=r[o];return e}function o(t){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<t.length;o+=3){var n=t[o],i=o+1<t.length,a=i?t[o+1]:0,s=o+2<t.length,u=s?t[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o<n;o++){var a=t[o];a instanceof h?t[o]=a._buffer:"string"==typeof a?t[o]=y(a):"DataView"===u(s=a)||f(i.DataView,s)?t[o]=e(a.buffer):c(a)?t[o]=e(a):t[o]=y(String(a))}var s;this._buffer=i.Uint8Array?function(t){for(var e=0,r=t.length;r--;)e+=t[r].length;for(var o=new Uint8Array(e),n=0,i=0;i<t.length;i++){var a=t[i];o.set(a,n),n+=a.byteLength||a.length}return o}(t):[].concat.apply([],t),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
||||
(()=>{var e={725:function(e,t,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(e){"use strict";var t=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(e,t){return(t=document.createElement("a")).href=e,t},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=t&&t.prototype.append&&t.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(e){}function c(e){return e.map((function(e){if(e.buffer instanceof ArrayBuffer){var t=e.buffer;if(e.byteLength!==t.byteLength){var r=new Uint8Array(e.byteLength);r.set(new Uint8Array(t,e.byteOffset,e.byteLength)),t=r.buffer}return t}return e}))}function h(e,r){r=r||{};var o=new t;return c(e).forEach((function(e){o.append(e)})),r.type?o.getBlob(r.type):o.getBlob()}function d(e,t){return new o(c(e),t||{})}i.Blob&&(h.prototype=Blob.prototype,d.prototype=Blob.prototype);var p="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(e){for(var t=0,r=e.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);t<r;){var u=e.charCodeAt(t++);if(u>=55296&&u<=56319){if(t<r){var f=e.charCodeAt(t);56320==(64512&f)&&(++t,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+t/e.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},y="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(e){for(var t=e.length,r=[],o=0;o<t;){var n,i,a,s,u=e[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=t)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=e[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=e[o+1],i=e[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=e[o+1],i=e[o+2],a=e[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",d=0;d<c;)h+=String.fromCharCode.apply(String,r.slice(d,d+=4096));return h};function b(){var t=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;t&&r&&(XMLHttpRequest.prototype.send=function(e){e instanceof Blob?(this.setRequestHeader("Content-Type",e.type),r.call(this,e)):r.call(this,e)});try{new File([],""),e.File=i.File,e.FileReader=i.FileReader}catch(t){try{e.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(t){e.File=function(e,t,r){var o=new Blob(e,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=t.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),e.Blob=f?i.Blob:d):l?(b(),e.Blob=h):function(){function t(e){for(var t=new Array(e.byteLength),r=new Uint8Array(e),o=t.length;o--;)t[o]=r[o];return t}function o(e){for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<e.length;o+=3){var n=e[o],i=o+1<e.length,a=i?e[o+1]:0,s=o+2<e.length,u=s?e[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(t[f],t[l],t[c],t[h])}return r.join("")}var s=Object.create||function(e){function t(){}return t.prototype=e,new t};function u(e){return Object.prototype.toString.call(e).slice(8,-1)}function f(e,t){return"object"==typeof e&&Object.prototype.isPrototypeOf.call(e.prototype,t)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(e){return t=l,r=u(e),-1!==t.indexOf(r)||f(i.ArrayBuffer,e);var t,r}function h(e,r){r=null==r?{}:r;for(var o=0,n=(e=e?e.slice():[]).length;o<n;o++){var a=e[o];a instanceof h?e[o]=a._buffer:"string"==typeof a?e[o]=p(a):"DataView"===u(s=a)||f(i.DataView,s)?e[o]=t(a.buffer):c(a)?e[o]=t(a):e[o]=p(String(a))}var s;this._buffer=i.Uint8Array?function(e){for(var t=0,r=e.length;r--;)t+=e[r].length;for(var o=new Uint8Array(t),n=0,i=0;i<e.length;i++){var a=e[i];o.set(a,n),n+=a.byteLength||a.length}return o}(e):[].concat.apply([],e),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
||||
+1169
-15
File diff suppressed because it is too large.
Load diff
@@ -7,16 +7,19 @@
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "webpack --mode production --watch",
|
||||
"build": "webpack --mode production"
|
||||
"build": "webpack --mode production",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"patch-package": "^8.0.0",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"blob-polyfill": "^7.0.20220408",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
"fastestsmallesttextencoderdecoder": "^1.0.22",
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
diff --git a/node_modules/whatwg-fetch/dist/fetch.umd.js b/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||
index 7a0d852..604691e 100644
|
||||
--- a/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||
+++ b/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||
@@ -394,6 +394,7 @@
|
||||
}
|
||||
}());
|
||||
this.referrer = null;
|
||||
+ this.redirect = options.redirect || 'follow'
|
||||
|
||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||
@@ -606,6 +607,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ if ('redirect' in xhr) {
|
||||
+ xhr.redirect = request.redirect
|
||||
+ }
|
||||
+
|
||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||
var names = [];
|
||||
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||
diff --git a/node_modules/whatwg-fetch/fetch.js b/node_modules/whatwg-fetch/fetch.js
|
||||
index f39a983..d1fc903 100644
|
||||
--- a/node_modules/whatwg-fetch/fetch.js
|
||||
+++ b/node_modules/whatwg-fetch/fetch.js
|
||||
@@ -388,6 +388,7 @@ export function Request(input, options) {
|
||||
}
|
||||
}());
|
||||
this.referrer = null
|
||||
+ this.redirect = options.redirect || 'follow'
|
||||
|
||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||
@@ -600,6 +601,10 @@ export function fetch(input, init) {
|
||||
}
|
||||
}
|
||||
|
||||
+ if ('redirect' in xhr) {
|
||||
+ xhr.redirect = request.redirect
|
||||
+ }
|
||||
+
|
||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||
var names = [];
|
||||
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./blob/index.js"
|
||||
import "./crypto/index.js"
|
||||
// polyfill TextEncoder
|
||||
import 'fastestsmallesttextencoderdecoder';
|
||||
import "./fetch/index.js"
|
||||
@@ -38,7 +38,7 @@ const (
|
||||
EventOnResolve ActivationEvent = "onResolve"
|
||||
EventOnStart ActivationEvent = "onStart"
|
||||
EventOnError ActivationEvent = "onError"
|
||||
//EventOnDone ActivationEvent = "onDone"
|
||||
EventOnDone ActivationEvent = "onDone"
|
||||
)
|
||||
|
||||
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
||||
@@ -155,7 +155,13 @@ func (d *Downloader) DeleteExtension(identity string) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
return d.storage.Delete(bucketExtension, identity)
|
||||
if err = d.storage.Delete(bucketExtension, identity); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = d.storage.Delete(bucketExtensionStorage, identity); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Downloader) GetExtensions() []*Extension {
|
||||
@@ -199,7 +205,7 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
|
||||
return nil, err
|
||||
}
|
||||
proxyOptions := transport.ProxyOptions{}
|
||||
proxyUrl := d.cfg.ProxyUrl()
|
||||
proxyUrl := d.cfg.DownloaderStoreConfig.Proxy.ToUrl()
|
||||
if proxyUrl != nil {
|
||||
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
|
||||
proxyOptions.Username = proxyUrl.User.Username()
|
||||
@@ -298,6 +304,17 @@ func (d *Downloader) triggerOnError(task *Task, err error) {
|
||||
)
|
||||
}
|
||||
|
||||
func (d *Downloader) triggerOnDone(task *Task) {
|
||||
doTrigger(d,
|
||||
EventOnDone,
|
||||
task.Meta.Req,
|
||||
&OnErrorContext{
|
||||
Task: NewExtensionTask(d, task),
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
||||
// init extension global object
|
||||
gopeed := &Instance{
|
||||
@@ -341,7 +358,7 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
|
||||
req.Labels = make(map[string]string)
|
||||
}
|
||||
engine := engine.NewEngine(&engine.Config{
|
||||
ProxyURL: d.cfg.ProxyUrl(),
|
||||
ProxyConfig: d.cfg.Proxy,
|
||||
})
|
||||
defer engine.Close()
|
||||
err = engine.Runtime.Set("gopeed", gopeed)
|
||||
@@ -605,9 +622,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
|
||||
h.register(EventOnError, fn)
|
||||
}
|
||||
|
||||
//func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||
// h.register(HookEventOnDone, fn)
|
||||
//}
|
||||
func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||
h.register(EventOnDone, fn)
|
||||
}
|
||||
|
||||
type ExtensionInfo struct {
|
||||
Identity string `json:"identity"`
|
||||
@@ -681,6 +698,10 @@ type OnErrorContext struct {
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
type OnDoneContext struct {
|
||||
Task *Task `json:"task"`
|
||||
}
|
||||
|
||||
type ExtensionTask struct {
|
||||
*Task
|
||||
|
||||
@@ -688,8 +709,12 @@ type ExtensionTask struct {
|
||||
}
|
||||
|
||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||
newTask := task.clone()
|
||||
// Assign the pointer of the properties that the extension supports modification
|
||||
newTask.Meta = task.Meta
|
||||
newTask.Status = task.Status
|
||||
return &ExtensionTask{
|
||||
Task: task.clone(),
|
||||
Task: newTask,
|
||||
download: download,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,8 +247,45 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
||||
}
|
||||
// extension on error modify url and continue download
|
||||
task := downloader.GetTask(id)
|
||||
if task.Meta.Req.URL != "https://github.com" {
|
||||
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
||||
if task.Status != base.DownloadStatusDone {
|
||||
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_Extension_OnDone(t *testing.T) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_done", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errCh := make(chan error, 1)
|
||||
downloader.Listener(func(event *Event) {
|
||||
if event.Key == EventKeyFinally {
|
||||
errCh <- event.Err
|
||||
}
|
||||
})
|
||||
id, err := downloader.CreateDirect(&base.Request{
|
||||
URL: "https://github.com",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err = <-errCh:
|
||||
break
|
||||
case <-time.After(time.Second * 10):
|
||||
err = errors.New("timeout")
|
||||
}
|
||||
// wait for script execution
|
||||
time.Sleep(time.Millisecond * 3000)
|
||||
|
||||
if err != nil {
|
||||
panic("extension on done download error: " + err.Error())
|
||||
}
|
||||
// extension on error modify url and continue download
|
||||
task := downloader.GetTask(id)
|
||||
if task.Meta.Req.Labels["modified"] != "true" {
|
||||
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
|
||||
}
|
||||
if task.Status != base.DownloadStatusDone {
|
||||
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||
|
||||
+59
-63
@@ -1,15 +1,14 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -21,19 +20,21 @@ type ResolveResult struct {
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Protocol string `json:"protocol"`
|
||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||
Status base.Status `json:"status"`
|
||||
Protocol string `json:"protocol"`
|
||||
Uploading bool `json:"uploading"`
|
||||
Progress *Progress `json:"progress"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
fetcherBuilder fetcher.FetcherBuilder
|
||||
fetcherManager fetcher.FetcherManager
|
||||
fetcher fetcher.Fetcher
|
||||
timer *util.Timer
|
||||
statusLock *sync.Mutex
|
||||
lock *sync.Mutex
|
||||
speedArr []int64
|
||||
uploadSpeedArr []int64
|
||||
}
|
||||
|
||||
func NewTask() *Task {
|
||||
@@ -49,37 +50,71 @@ func NewTask() *Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the display name of the task.
|
||||
func (t *Task) Name() string {
|
||||
// Custom name first
|
||||
if t.Meta.Opts.Name != "" {
|
||||
return t.Meta.Opts.Name
|
||||
}
|
||||
|
||||
// Task is not resolved, parse the name from the URL
|
||||
if t.Meta.Res == nil {
|
||||
fallbackName := "unknown"
|
||||
if t.fetcherManager == nil {
|
||||
return fallbackName
|
||||
}
|
||||
parseName := t.fetcherManager.ParseName(t.Meta.Req.URL)
|
||||
if parseName == "" {
|
||||
return fallbackName
|
||||
}
|
||||
return parseName
|
||||
}
|
||||
|
||||
// Task is a folder
|
||||
if t.Meta.Res.Name != "" {
|
||||
return t.Meta.Res.Name
|
||||
}
|
||||
|
||||
// Get the name of the first file
|
||||
return t.Meta.Res.Files[0].Name
|
||||
}
|
||||
|
||||
func (t *Task) MarshalJSON() ([]byte, error) {
|
||||
type rawTaskType Task
|
||||
jsonTask := struct {
|
||||
rawTaskType
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
rawTaskType(*t),
|
||||
t.Name(),
|
||||
}
|
||||
return json.Marshal(jsonTask)
|
||||
}
|
||||
|
||||
func (t *Task) updateStatus(status base.Status) {
|
||||
t.UpdatedAt = time.Now()
|
||||
t.Status = status
|
||||
}
|
||||
|
||||
func (t *Task) clone() *Task {
|
||||
return &Task{
|
||||
ID: t.ID,
|
||||
Meta: t.Meta,
|
||||
Status: t.Status,
|
||||
Progress: t.Progress,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
return util.DeepClone(t)
|
||||
}
|
||||
|
||||
func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
|
||||
t.speedArr = append(t.speedArr, downloaded)
|
||||
if len(t.speedArr) > 6 {
|
||||
t.speedArr = t.speedArr[1:]
|
||||
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
||||
speedArr = append(speedArr, downloaded)
|
||||
if len(speedArr) > 6 {
|
||||
speedArr = speedArr[1:]
|
||||
}
|
||||
var total int64
|
||||
for _, v := range t.speedArr {
|
||||
for _, v := range speedArr {
|
||||
total += v
|
||||
}
|
||||
return int64(float64(total) / float64(len(t.speedArr)) / usedTime)
|
||||
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
||||
}
|
||||
|
||||
type DownloaderConfig struct {
|
||||
Controller *controller.Controller
|
||||
FetchBuilders []fetcher.FetcherBuilder
|
||||
FetchManagers []fetcher.FetcherManager
|
||||
|
||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||
Storage Storage
|
||||
@@ -87,17 +122,17 @@ type DownloaderConfig struct {
|
||||
|
||||
ProductionMode bool
|
||||
|
||||
*DownloaderStoreConfig
|
||||
*base.DownloaderStoreConfig
|
||||
}
|
||||
|
||||
func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
||||
if cfg.Controller == nil {
|
||||
cfg.Controller = controller.NewController()
|
||||
}
|
||||
if len(cfg.FetchBuilders) == 0 {
|
||||
cfg.FetchBuilders = []fetcher.FetcherBuilder{
|
||||
new(http.FetcherBuilder),
|
||||
new(bt.FetcherBuilder),
|
||||
if len(cfg.FetchManagers) == 0 {
|
||||
cfg.FetchManagers = []fetcher.FetcherManager{
|
||||
new(http.FetcherManager),
|
||||
new(bt.FetcherManager),
|
||||
}
|
||||
}
|
||||
if cfg.RefreshInterval == 0 {
|
||||
@@ -108,42 +143,3 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||
type DownloaderStoreConfig struct {
|
||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||
|
||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||
Extra map[string]any `json:"extra"`
|
||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.MaxRunning == 0 {
|
||||
cfg.MaxRunning = 5
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) ProxyUrl() *url.URL {
|
||||
if cfg.Proxy == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.Proxy.Enable == false || cfg.Proxy.Scheme == "" || cfg.Proxy.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return util.BuildProxyUrl(cfg.Proxy.Scheme, cfg.Proxy.Host, cfg.Proxy.Usr, cfg.Proxy.Pwd)
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
Scheme string `json:"scheme"`
|
||||
Host string `json:"host"`
|
||||
Usr string `json:"usr"`
|
||||
Pwd string `json:"pwd"`
|
||||
}
|
||||
@@ -69,7 +69,9 @@ func (n *MemStorage) Setup(buckets []string) error {
|
||||
func (n *MemStorage) Put(bucket string, key string, v any) error {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
memData[bucket][key] = v
|
||||
if bucketData, ok := memData[bucket]; ok {
|
||||
bucketData[key] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
gopeed.events.onDone(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
ctx.task.meta.req.labels['modified'] = 'true';
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "on-done",
|
||||
"title": "gopeed extension on done event test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onDone",
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://github.com/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,7 +2,6 @@ gopeed.events.onError(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
gopeed.logger.info("error", ctx.error);
|
||||
ctx.task.meta.req.url = "https://github.com";
|
||||
ctx.task.pause();
|
||||
ctx.task.continue();
|
||||
});
|
||||
|
||||
@@ -4,11 +4,16 @@ type ReqExtra struct {
|
||||
Trackers []string `json:"trackers"`
|
||||
}
|
||||
|
||||
// Stats for download
|
||||
// Stats for torrent
|
||||
type Stats struct {
|
||||
// bt stats
|
||||
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
||||
TotalPeers int `json:"totalPeers"`
|
||||
ActivePeers int `json:"activePeers"`
|
||||
ConnectedSeeders int `json:"connectedSeeders"`
|
||||
// Total seed bytes
|
||||
SeedBytes int64 `json:"seedBytes"`
|
||||
// Seed ratio
|
||||
SeedRatio float64 `json:"seedRatio"`
|
||||
// Total seed time
|
||||
SeedTime int64 `json:"seedTime"`
|
||||
}
|
||||
Loaded 50 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user