mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 11:53:41 +00:00
Compare 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 |
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
.git
|
||||||
|
|
||||||
|
*.data
|
||||||
|
*.log
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
.github
|
||||||
|
_docs
|
||||||
|
_examples
|
||||||
|
bin
|
||||||
|
ui
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* @monkeyWie
|
||||||
@@ -11,6 +11,9 @@ categories:
|
|||||||
- 'fix'
|
- 'fix'
|
||||||
- 'bugfix'
|
- 'bugfix'
|
||||||
- 'bug'
|
- 'bug'
|
||||||
|
- title: '🔧 Performance Improvements'
|
||||||
|
labels:
|
||||||
|
- 'perf'
|
||||||
- title: '🧪 Tests'
|
- title: '🧪 Tests'
|
||||||
label: 'test'
|
label: 'test'
|
||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ on:
|
|||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.21"
|
GO_VERSION: "1.22"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -380,7 +380,7 @@ jobs:
|
|||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
go get golang.org/x/mobile/bind
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
gomobile init
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
flutter build apk
|
flutter build apk
|
||||||
@@ -483,6 +483,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo rm -rf /usr/share/dotnet
|
sudo rm -rf /usr/share/dotnet
|
||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
@@ -493,11 +496,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: liwei2633
|
username: liwei2633
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build flutter web
|
||||||
|
run: |
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web --web-renderer html
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
with:
|
with:
|
||||||
|
context: .
|
||||||
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ env.VERSION }}
|
VERSION=${{ env.VERSION }}
|
||||||
@@ -507,7 +518,6 @@ jobs:
|
|||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm64
|
linux/arm64
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
linux/arm64/v8
|
|
||||||
tags: |
|
tags: |
|
||||||
liwei2633/gopeed:latest
|
liwei2633/gopeed:latest
|
||||||
liwei2633/gopeed:v${{ env.VERSION }}
|
liwei2633/gopeed:v${{ env.VERSION }}
|
||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.22"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.21"
|
GO_VERSION: "1.22"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -54,9 +54,10 @@ jobs:
|
|||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Run Unit tests.
|
- name: Run Unit tests.
|
||||||
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
|
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
|
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||||
build:
|
build:
|
||||||
name: Build check
|
name: Build check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+1
-9
@@ -1,16 +1,8 @@
|
|||||||
FROM instrumentisto/flutter:3.16.8 AS flutter
|
FROM golang:1.22.6 AS go
|
||||||
WORKDIR /app
|
|
||||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
|
||||||
RUN flutter pub get
|
|
||||||
COPY ./ui/flutter ./
|
|
||||||
RUN flutter build web --web-renderer html
|
|
||||||
|
|
||||||
FROM golang:1.21.6 AS go
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./go.mod ./go.sum ./
|
COPY ./go.mod ./go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=flutter /app/build/web ./cmd/web/dist
|
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
RUN CGO_ENABLED=0 go build -tags nosqlite,web \
|
RUN CGO_ENABLED=0 go build -tags nosqlite,web \
|
||||||
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|

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

|

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

|

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

|

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

|

|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
@@ -108,7 +110,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|||||||
|
|
||||||
### 開發環境
|
### 開發環境
|
||||||
|
|
||||||
1. Golang 1.21+
|
1. Golang 1.22+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆項目
|
### 克隆項目
|
||||||
@@ -168,7 +170,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+26
-1
@@ -8,7 +8,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed banner.txt
|
//go:embed banner.txt
|
||||||
@@ -25,8 +27,17 @@ func Start(cfg *model.StartConfig) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if downloadCfg.FirstLoad {
|
if downloadCfg.FirstLoad {
|
||||||
|
// Set default download config
|
||||||
|
if cfg.DownloadConfig != nil {
|
||||||
|
cfg.DownloadConfig.Merge(downloadCfg)
|
||||||
|
// TODO Use PatchConfig
|
||||||
|
rest.Downloader.PutConfig(cfg.DownloadConfig)
|
||||||
|
downloadCfg = cfg.DownloadConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadDir := downloadCfg.DownloadDir
|
||||||
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
|
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
|
||||||
var downloadDir string
|
if downloadDir == "" {
|
||||||
if base.InDocker == "true" {
|
if base.InDocker == "true" {
|
||||||
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||||
} else {
|
} else {
|
||||||
@@ -40,8 +51,22 @@ func Start(cfg *model.StartConfig) {
|
|||||||
rest.Downloader.PutConfig(downloadCfg)
|
rest.Downloader.PutConfig(downloadCfg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
watchExit()
|
||||||
|
|
||||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func watchExit() {
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
sig := <-sigs
|
||||||
|
fmt.Printf("Server is shutting down due to signal: %s\n", sig)
|
||||||
|
rest.Downloader.Close()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
}
|
||||||
+6
-2
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,8 @@ type args struct {
|
|||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
ApiToken *string `json:"apiToken"`
|
ApiToken *string `json:"apiToken"`
|
||||||
StorageDir *string `json:"storageDir"`
|
StorageDir *string `json:"storageDir"`
|
||||||
|
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||||
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
configPath *string
|
configPath *string
|
||||||
}
|
}
|
||||||
@@ -25,7 +28,7 @@ func parse() *args {
|
|||||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
||||||
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
|
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
|
||||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -57,8 +60,9 @@ func loadConfig(path string) *args {
|
|||||||
var args args
|
var args args
|
||||||
|
|
||||||
if !filepath.IsAbs(path) {
|
if !filepath.IsAbs(path) {
|
||||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println("config dir get failed, reason:" + err.Error())
|
||||||
return &args
|
return &args
|
||||||
}
|
}
|
||||||
path = filepath.Join(dir, path)
|
path = filepath.Join(dir, path)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func main() {
|
|||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
StorageDir: filepath.Join(dir, "storage"),
|
StorageDir: filepath.Join(dir, "storage"),
|
||||||
ApiToken: *args.ApiToken,
|
ApiToken: *args.ApiToken,
|
||||||
|
DownloadConfig: args.DownloadConfig,
|
||||||
ProductionMode: true,
|
ProductionMode: true,
|
||||||
WebEnable: true,
|
WebEnable: true,
|
||||||
WebFS: sub,
|
WebFS: sub,
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
chown -R ${PUID}:${PGID} /app
|
chown -R ${PUID}:${PGID} /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,58 @@
|
|||||||
module github.com/GopeedLab/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||||
github.com/go-git/go-git/v5 v5.8.1
|
github.com/go-git/go-git/v5 v5.8.1
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
github.com/mattn/go-ieproxy v0.0.12
|
github.com/mattn/go-ieproxy v0.0.12
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.3.8
|
go.etcd.io/bbolt v1.3.10
|
||||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||||
golang.org/x/sync v0.6.0
|
golang.org/x/sync v0.8.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.7.0 // indirect
|
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/chansync v0.4.0 // indirect
|
github.com/anacrolix/chansync v0.5.1 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
github.com/anacrolix/dht/v2 v2.21.1 // indirect
|
||||||
github.com/anacrolix/envpprof v1.3.0 // 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/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 v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
||||||
github.com/anacrolix/stm v0.5.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.5.1 // indirect
|
github.com/anacrolix/sync v0.5.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/anacrolix/utp v0.2.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
@@ -58,61 +60,69 @@ require (
|
|||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||||
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||||
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
|
github.com/go-llsqlite/crawshaw v0.5.3 // indirect
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.1 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.4.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.5 // indirect
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/pion/ice/v2 v2.3.12 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pion/interceptor v0.1.25 // indirect
|
github.com/pion/datachannel v1.5.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/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/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.13 // indirect
|
github.com/pion/rtcp v1.2.14 // indirect
|
||||||
github.com/pion/rtp v1.8.3 // indirect
|
github.com/pion/rtp v1.8.7 // indirect
|
||||||
github.com/pion/sctp v1.8.9 // indirect
|
github.com/pion/sctp v1.8.19 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
github.com/pion/srtp/v2 v2.0.20 // indirect
|
||||||
github.com/pion/stun v0.6.1 // indirect
|
github.com/pion/stun v0.6.1 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
github.com/pion/transport/v2 v2.2.9 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||||
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
github.com/pion/webrtc/v3 v3.2.50 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
github.com/stretchr/testify v1.8.4 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.9.0 // indirect
|
||||||
github.com/tidwall/btree v1.7.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/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
golang.org/x/mod v0.14.0 // indirect
|
golang.org/x/mod v0.19.0 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/net v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/time v0.5.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/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.7.2 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.28.0 // indirect
|
modernc.org/sqlite v1.31.1 // indirect
|
||||||
zombiezen.com/go/sqlite v1.1.0 // indirect
|
zombiezen.com/go/sqlite v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -2,13 +2,15 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
GetConfig func(v any) bool
|
GetConfig func(v any)
|
||||||
ProxyConfig *base.DownloaderProxyConfig
|
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
||||||
FileController
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
}
|
}
|
||||||
@@ -22,10 +24,9 @@ type DefaultFileController struct {
|
|||||||
|
|
||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
|
GetConfig: func(v any) {},
|
||||||
|
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
GetConfig: func(v any) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetcher defines the interface for a download protocol.
|
// Fetcher defines the interface for a download protocol.
|
||||||
// One fetcher for each download task
|
// Each download task will have a corresponding Fetcher instance for the management of the download task
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
// Name return the name of the protocol.
|
|
||||||
Name() string
|
|
||||||
|
|
||||||
Setup(ctl *controller.Controller)
|
Setup(ctl *controller.Controller)
|
||||||
// Resolve resource info from request
|
// Resolve resource info from request
|
||||||
Resolve(req *base.Request) error
|
Resolve(req *base.Request) error
|
||||||
@@ -31,6 +29,12 @@ type Fetcher interface {
|
|||||||
Wait() error
|
Wait() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Uploader interface {
|
||||||
|
Upload() error
|
||||||
|
UploadedBytes() int64
|
||||||
|
WaitUpload() error
|
||||||
|
}
|
||||||
|
|
||||||
// FetcherMeta defines the meta information of a fetcher.
|
// FetcherMeta defines the meta information of a fetcher.
|
||||||
type FetcherMeta struct {
|
type FetcherMeta struct {
|
||||||
Req *base.Request `json:"req"`
|
Req *base.Request `json:"req"`
|
||||||
@@ -68,17 +72,57 @@ func (m *FetcherMeta) RootDirPath() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetcherBuilder defines the interface for a fetcher builder.
|
type FilterType int
|
||||||
type FetcherBuilder interface {
|
|
||||||
// Schemes returns the schemes supported by the fetcher.
|
const (
|
||||||
Schemes() []string
|
// FilterTypeUrl url type, pattern is the scheme, e.g. http://github.com -> http
|
||||||
|
FilterTypeUrl FilterType = iota
|
||||||
|
// FilterTypeFile file type, pattern is the file extension name, e.g. test.torrent -> torrent
|
||||||
|
FilterTypeFile
|
||||||
|
// FilterTypeBase64 base64 data type, pattern is the data mime type, e.g. data:application/x-bittorrent;base64 -> application/x-bittorrent
|
||||||
|
FilterTypeBase64
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchemeFilter struct {
|
||||||
|
Type FilterType
|
||||||
|
Pattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SchemeFilter) Match(uri string) bool {
|
||||||
|
uriUpper := strings.ToUpper(uri)
|
||||||
|
patternUpper := strings.ToUpper(s.Pattern)
|
||||||
|
switch s.Type {
|
||||||
|
case FilterTypeUrl:
|
||||||
|
return strings.HasPrefix(uriUpper, patternUpper+":")
|
||||||
|
case FilterTypeFile:
|
||||||
|
return strings.HasSuffix(uriUpper, "."+patternUpper)
|
||||||
|
case FilterTypeBase64:
|
||||||
|
return strings.HasPrefix(uriUpper, "DATA:"+patternUpper+";BASE64,")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetcherManager manage and control the fetcher
|
||||||
|
type FetcherManager interface {
|
||||||
|
// Name return the name of the protocol.
|
||||||
|
Name() string
|
||||||
|
// Filters registers the supported schemes.
|
||||||
|
Filters() []*SchemeFilter
|
||||||
// Build returns a new fetcher.
|
// Build returns a new fetcher.
|
||||||
Build() Fetcher
|
Build() Fetcher
|
||||||
|
// ParseName name displayed when the task is not yet resolved, parsed from the request URL
|
||||||
|
ParseName(u string) string
|
||||||
|
// AutoRename returns whether the fetcher need renaming the download file when has the same name file.
|
||||||
|
AutoRename() bool
|
||||||
|
|
||||||
|
// DefaultConfig returns the default configuration of the protocol.
|
||||||
|
DefaultConfig() any
|
||||||
// Store fetcher
|
// Store fetcher
|
||||||
Store(fetcher Fetcher) (any, error)
|
Store(fetcher Fetcher) (any, error)
|
||||||
// Restore fetcher
|
// Restore fetcher
|
||||||
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
||||||
|
// Close the fetcher manager, release resources.
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultFetcher struct {
|
type DefaultFetcher struct {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package fetcher
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSchemeFilter_Match(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Type FilterType
|
||||||
|
Pattern string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
uri string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "url match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeUrl,
|
||||||
|
Pattern: "https",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "https://github.com",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "url not match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeUrl,
|
||||||
|
Pattern: "https",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "ftp://github.com",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeFile,
|
||||||
|
Pattern: "torrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "d:/temp/test.torrent",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file not match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeFile,
|
||||||
|
Pattern: "torrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "d:/temp/test.txt",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base64 match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeBase64,
|
||||||
|
Pattern: "application/x-bittorrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "data:application/x-bittorrent;base64,xxx",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base64 not match",
|
||||||
|
fields: fields{
|
||||||
|
Type: FilterTypeBase64,
|
||||||
|
Pattern: "application/x-bittorrent",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
uri: "data:application/javascript;base64,xxx",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &SchemeFilter{
|
||||||
|
Type: tt.fields.Type,
|
||||||
|
Pattern: tt.fields.Pattern,
|
||||||
|
}
|
||||||
|
if got := s.Match(tt.args.uri); got != tt.want {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,4 +3,10 @@ package bt
|
|||||||
type config struct {
|
type config struct {
|
||||||
ListenPort int `json:"listenPort"`
|
ListenPort int `json:"listenPort"`
|
||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
|
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
||||||
|
SeedKeep bool `json:"seedKeep`
|
||||||
|
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||||
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
|
SeedTime int64 `json:"seedTime"`
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/dnscache"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DnsCacheResolver resolves DNS requests for an HTTP client using an in-memory cache.
|
||||||
|
type DnsCacheResolver struct {
|
||||||
|
RefreshTimeout time.Duration
|
||||||
|
|
||||||
|
resolver dnscache.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DnsCacheResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ips, err := r.resolver.LookupHost(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var conn net.Conn
|
||||||
|
for _, ip := range ips {
|
||||||
|
var dialer net.Dialer
|
||||||
|
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip, port))
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DnsCacheResolver) Run(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(r.RefreshTimeout)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
r.resolver.Refresh(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+273
-75
File diff suppressed because it is too large.
Load diff
@@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -33,7 +35,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
|
||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -69,6 +70,7 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
|
t.Run("Resolve Single File", func(t *testing.T) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
Extra: bt.ReqExtra{
|
Extra: bt.ReqExtra{
|
||||||
@@ -83,7 +85,6 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
|
||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -97,29 +98,123 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
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
|
||||||
|
}
|
||||||
|
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 {
|
func buildFetcher() fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fb := new(FetcherManager)
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher := fb.Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
newController.GetConfig = func(v any) {
|
||||||
|
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
|
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherManager).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
mockCfg := config{
|
mockCfg := config{
|
||||||
Trackers: []string{
|
Trackers: []string{
|
||||||
"udp://tracker.birkenwald.de:6969/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
}}
|
}}
|
||||||
newController.GetConfig = func(v any) bool {
|
newController.GetConfig = func(v any) {
|
||||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
return proxyConfig.ToHandler()
|
||||||
}
|
}
|
||||||
newController.ProxyConfig = proxyConfig
|
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
BIN
Binary file not shown.
@@ -5,12 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
|
||||||
"github.com/xiaoqidun/setft"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -21,6 +15,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/xiaoqidun/setft"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
@@ -64,23 +65,13 @@ type Fetcher struct {
|
|||||||
eg *errgroup.Group
|
eg *errgroup.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Name() string {
|
|
||||||
return "http"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||||
f.ctl = ctl
|
f.ctl = ctl
|
||||||
f.doneCh = make(chan error, 1)
|
f.doneCh = make(chan error, 1)
|
||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
exist := f.ctl.GetConfig(&f.config)
|
f.ctl.GetConfig(&f.config)
|
||||||
if !exist {
|
|
||||||
f.config = &config{
|
|
||||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
|
||||||
Connections: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +79,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
f.meta.Req = req
|
||||||
httpReq, err := f.buildRequest(nil, req)
|
httpReq, err := f.buildRequest(nil, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -108,7 +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)) {
|
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
||||||
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
|
// response 206 status code, support breakpoint continuation
|
||||||
res.Range = true
|
res.Range = true
|
||||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||||
@@ -120,7 +112,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
res.Size = parse
|
res.Size = parse
|
||||||
}
|
}
|
||||||
} else if base.HttpCodeOK == httpResp.StatusCode {
|
} else if base.HttpCodeOK == httpResp.StatusCode {
|
||||||
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
// response 200 status code, not support breakpoint continuation, get file size by Content-Length header
|
||||||
|
// if not found, maybe chunked encoding
|
||||||
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||||
@@ -161,7 +154,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
file.Name = httpReq.URL.Hostname()
|
file.Name = httpReq.URL.Hostname()
|
||||||
}
|
}
|
||||||
res.Files = append(res.Files, file)
|
res.Files = append(res.Files, file)
|
||||||
f.meta.Req = req
|
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -179,8 +171,12 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
|||||||
opts.Extra = &fhttp.OptsExtra{}
|
opts.Extra = &fhttp.OptsExtra{}
|
||||||
}
|
}
|
||||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||||
if extra.Connections == 0 {
|
if extra.Connections <= 0 {
|
||||||
extra.Connections = f.config.Connections
|
extra.Connections = f.config.Connections
|
||||||
|
// Avoid zero connections configuration
|
||||||
|
if extra.Connections <= 0 {
|
||||||
|
extra.Connections = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -386,7 +382,8 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
method string
|
method string
|
||||||
body io.Reader
|
body io.Reader
|
||||||
)
|
)
|
||||||
headers := make(map[string][]string)
|
|
||||||
|
headers := http.Header{}
|
||||||
if req.Extra == nil {
|
if req.Extra == nil {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
} else {
|
} else {
|
||||||
@@ -398,7 +395,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
if len(extra.Header) > 0 {
|
if len(extra.Header) > 0 {
|
||||||
for k, v := range extra.Header {
|
for k, v := range extra.Header {
|
||||||
headers[k] = []string{v}
|
headers.Set(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if extra.Body != "" {
|
if extra.Body != "" {
|
||||||
@@ -406,8 +403,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||||
// load user agent from config
|
headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
|
||||||
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
@@ -452,7 +448,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
|||||||
|
|
||||||
func (f *Fetcher) buildClient() *http.Client {
|
func (f *Fetcher) buildClient() *http.Client {
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
Proxy: f.ctl.ProxyConfig.ToHandler(),
|
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||||
}
|
}
|
||||||
// Cookie handle
|
// Cookie handle
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
@@ -466,30 +462,67 @@ type fetcherData struct {
|
|||||||
Chunks []*chunk
|
Chunks []*chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherBuilder struct {
|
type FetcherManager struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var schemes = []string{"HTTP", "HTTPS"}
|
func (fm *FetcherManager) Name() string {
|
||||||
|
return "http"
|
||||||
func (fb *FetcherBuilder) Schemes() []string {
|
|
||||||
return schemes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||||
|
return []*fetcher.SchemeFilter{
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeUrl,
|
||||||
|
Pattern: "HTTP",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeUrl,
|
||||||
|
Pattern: "HTTPS",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
|
var name string
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Get filePath by URL
|
||||||
|
name = path.Base(url.Path)
|
||||||
|
// If file name is empty, use host name
|
||||||
|
if name == "" || name == "/" || name == "." {
|
||||||
|
name = url.Hostname()
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
|
return &config{
|
||||||
|
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||||
|
Connections: 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
_f := f.(*Fetcher)
|
_f := f.(*Fetcher)
|
||||||
return &fetcherData{
|
return &fetcherData{
|
||||||
Chunks: _f.chunks,
|
Chunks: _f.chunks,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
fd := v.(*fetcherData)
|
fd := v.(*fetcherData)
|
||||||
fb := &FetcherBuilder{}
|
fb := &FetcherManager{}
|
||||||
fetcher := fb.Build().(*Fetcher)
|
fetcher := fb.Build().(*Fetcher)
|
||||||
fetcher.meta = meta
|
fetcher.meta = meta
|
||||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
@@ -500,3 +533,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"net"
|
"net"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -192,6 +194,54 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken url",
|
||||||
|
args: args{
|
||||||
|
u: "https://!@#%github.com",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/index.html",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path with query and hash",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/a/b/index.html/#list?name=1",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com",
|
||||||
|
},
|
||||||
|
want: "github.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||||
|
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||||
return doDownloadReady(buildFetcher(), listener, connections, t)
|
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||||
}
|
}
|
||||||
@@ -306,7 +356,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fb := new(FetcherBuilder)
|
fb := new(FetcherManager)
|
||||||
time.Sleep(time.Millisecond * 50)
|
time.Sleep(time.Millisecond * 50)
|
||||||
data, err := fb.Store(fetcher)
|
data, err := fb.Store(fetcher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -337,10 +387,12 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||||
fetcher := downloadReady(httpListener, 4, t)
|
fetcher := downloadReady(httpListener, 4, t)
|
||||||
ctl := controller.NewController()
|
ctl := controller.NewController()
|
||||||
ctl.ProxyConfig = &base.DownloaderProxyConfig{
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
return (&base.DownloaderProxyConfig{
|
||||||
Enable: true,
|
Enable: true,
|
||||||
Scheme: "socks5",
|
Scheme: "socks5",
|
||||||
Host: proxyListener.Addr().String(),
|
Host: proxyListener.Addr().String(),
|
||||||
|
}).ToHandler()
|
||||||
}
|
}
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
@@ -359,19 +411,21 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildFetcher() *Fetcher {
|
func buildFetcher() *Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fm := new(FetcherManager)
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher := fm.Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
newController.GetConfig = func(v any) {
|
||||||
|
json.Unmarshal([]byte(test.ToJson(fm.DefaultConfig())), v)
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
return fetcher.(*Fetcher)
|
return fetcher.(*Fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherManager).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
newController.GetConfig = func(v any) bool {
|
newController.GetConfig = func(v any) {
|
||||||
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
json.Unmarshal([]byte(test.ToJson(cfg)), v)
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/armon/go-socks5"
|
"github.com/armon/go-socks5"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|||||||
+59
-6
@@ -16,6 +16,8 @@ type Request struct {
|
|||||||
Extra any `json:"extra"`
|
Extra any `json:"extra"`
|
||||||
// Labels is used to mark the download task
|
// Labels is used to mark the download task
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
|
// Proxy is special proxy config for request
|
||||||
|
Proxy *RequestProxy `json:"proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Validate() error {
|
func (r *Request) Validate() error {
|
||||||
@@ -25,6 +27,37 @@ func (r *Request) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestProxyMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RequestProxyModeFollow follow setting proxy
|
||||||
|
RequestProxyModeFollow RequestProxyMode = "follow"
|
||||||
|
// RequestProxyModeNone not use proxy
|
||||||
|
RequestProxyModeNone RequestProxyMode = "none"
|
||||||
|
// RequestProxyModeCustom custom proxy
|
||||||
|
RequestProxyModeCustom RequestProxyMode = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestProxy struct {
|
||||||
|
Mode RequestProxyMode `json:"mode"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Usr string `json:"usr"`
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||||
|
if p == nil || p.Mode != RequestProxyModeCustom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Scheme == "" || p.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd))
|
||||||
|
}
|
||||||
|
|
||||||
// Resource download resource
|
// Resource download resource
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
// if name is not empty, the resource is a folder and the name is the folder name
|
// if name is not empty, the resource is a folder and the name is the folder name
|
||||||
@@ -94,12 +127,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) Clone() *Options {
|
func (o *Options) Clone() *Options {
|
||||||
return &Options{
|
return util.DeepClone(o)
|
||||||
Name: o.Name,
|
|
||||||
Path: o.Path,
|
|
||||||
SelectFiles: o.SelectFiles,
|
|
||||||
Extra: o.Extra,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseReqExtra[E any](req *Request) error {
|
func ParseReqExtra[E any](req *Request) error {
|
||||||
@@ -147,12 +175,37 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
|||||||
if cfg.MaxRunning == 0 {
|
if cfg.MaxRunning == 0 {
|
||||||
cfg.MaxRunning = 5
|
cfg.MaxRunning = 5
|
||||||
}
|
}
|
||||||
|
if cfg.ProtocolConfig == nil {
|
||||||
|
cfg.ProtocolConfig = make(map[string]any)
|
||||||
|
}
|
||||||
if cfg.Proxy == nil {
|
if cfg.Proxy == nil {
|
||||||
cfg.Proxy = &DownloaderProxyConfig{}
|
cfg.Proxy = &DownloaderProxyConfig{}
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *DownloaderStoreConfig {
|
||||||
|
if beforeCfg == nil {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
if cfg.DownloadDir == "" {
|
||||||
|
cfg.DownloadDir = beforeCfg.DownloadDir
|
||||||
|
}
|
||||||
|
if cfg.MaxRunning == 0 {
|
||||||
|
cfg.MaxRunning = beforeCfg.MaxRunning
|
||||||
|
}
|
||||||
|
if cfg.ProtocolConfig == nil {
|
||||||
|
cfg.ProtocolConfig = beforeCfg.ProtocolConfig
|
||||||
|
}
|
||||||
|
if cfg.Extra == nil {
|
||||||
|
cfg.Extra = beforeCfg.Extra
|
||||||
|
}
|
||||||
|
if cfg.Proxy == nil {
|
||||||
|
cfg.Proxy = beforeCfg.Proxy
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
type DownloaderProxyConfig struct {
|
type DownloaderProxyConfig struct {
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
// System is the flag that use system proxy
|
// System is the flag that use system proxy
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+181
-72
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/pkgerrors"
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
"github.com/virtuald/go-paniclog"
|
"github.com/virtuald/go-paniclog"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -42,12 +44,16 @@ var (
|
|||||||
type Listener func(event *Event)
|
type Listener func(event *Event)
|
||||||
|
|
||||||
type Progress struct {
|
type Progress struct {
|
||||||
// 下载耗时(纳秒)
|
// Total download time(ns)
|
||||||
Used int64 `json:"used"`
|
Used int64 `json:"used"`
|
||||||
// 每秒下载字节数
|
// Download speed(bytes/s)
|
||||||
Speed int64 `json:"speed"`
|
Speed int64 `json:"speed"`
|
||||||
// 已下载的字节数
|
// Downloaded size(bytes)
|
||||||
Downloaded int64 `json:"downloaded"`
|
Downloaded int64 `json:"downloaded"`
|
||||||
|
// Uploaded speed(bytes/s)
|
||||||
|
UploadSpeed int64 `json:"uploadSpeed"`
|
||||||
|
// Uploaded size(bytes)
|
||||||
|
Uploaded int64 `json:"uploaded"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Downloader struct {
|
type Downloader struct {
|
||||||
@@ -55,11 +61,11 @@ type Downloader struct {
|
|||||||
ExtensionLogger *logger.Logger
|
ExtensionLogger *logger.Logger
|
||||||
|
|
||||||
cfg *DownloaderConfig
|
cfg *DownloaderConfig
|
||||||
fetcherBuilders map[string]fetcher.FetcherBuilder
|
|
||||||
fetcherCache map[string]fetcher.Fetcher
|
fetcherCache map[string]fetcher.Fetcher
|
||||||
storage Storage
|
storage Storage
|
||||||
tasks []*Task
|
tasks []*Task
|
||||||
waitTasks []*Task
|
waitTasks []*Task
|
||||||
|
watchedTasks sync.Map
|
||||||
listener Listener
|
listener Listener
|
||||||
|
|
||||||
lock *sync.Mutex
|
lock *sync.Mutex
|
||||||
@@ -78,7 +84,6 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
|||||||
|
|
||||||
d := &Downloader{
|
d := &Downloader{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
fetcherBuilders: make(map[string]fetcher.FetcherBuilder),
|
|
||||||
fetcherCache: make(map[string]fetcher.Fetcher),
|
fetcherCache: make(map[string]fetcher.Fetcher),
|
||||||
waitTasks: make([]*Task, 0),
|
waitTasks: make([]*Task, 0),
|
||||||
storage: cfg.Storage,
|
storage: cfg.Storage,
|
||||||
@@ -89,11 +94,6 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
|||||||
|
|
||||||
extensions: make([]*Extension, 0),
|
extensions: make([]*Extension, 0),
|
||||||
}
|
}
|
||||||
for _, f := range cfg.FetchBuilders {
|
|
||||||
for _, p := range f.Schemes() {
|
|
||||||
d.fetcherBuilders[strings.ToUpper(p)] = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||||
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
|
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
|
||||||
@@ -124,11 +124,14 @@ func (d *Downloader) Setup() error {
|
|||||||
}
|
}
|
||||||
// init default config
|
// init default config
|
||||||
d.cfg.DownloaderStoreConfig.Init()
|
d.cfg.DownloaderStoreConfig.Init()
|
||||||
for _, fb := range d.fetcherBuilders {
|
// init protocol config, if not exist, use default config
|
||||||
f := fb.Build()
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
d.setupFetcher(f)
|
protocol := fm.Name()
|
||||||
|
if _, ok := d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol]; !ok {
|
||||||
|
d.cfg.DownloaderStoreConfig.ProtocolConfig[protocol] = fm.DefaultConfig()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load tasks from storage
|
// load tasks from storage
|
||||||
var tasks []*Task
|
var tasks []*Task
|
||||||
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
||||||
@@ -166,6 +169,22 @@ func (d *Downloader) Setup() error {
|
|||||||
}
|
}
|
||||||
d.extensions = extensions
|
d.extensions = extensions
|
||||||
|
|
||||||
|
// handle upload
|
||||||
|
go func() {
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
if task.Status == base.DownloadStatusDone && task.Uploading {
|
||||||
|
if err := d.restoreTask(task); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("task upload restore fetcher failed, task id: %s", task.ID)
|
||||||
|
}
|
||||||
|
if uploader, ok := task.fetcher.(fetcher.Uploader); ok {
|
||||||
|
if err := uploader.Upload(); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("task upload failed, task id: %s", task.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// calculate download speed every tick
|
// calculate download speed every tick
|
||||||
go func() {
|
go func() {
|
||||||
for !d.closed.Load() {
|
for !d.closed.Load() {
|
||||||
@@ -174,22 +193,31 @@ func (d *Downloader) Setup() error {
|
|||||||
func() {
|
func() {
|
||||||
task.statusLock.Lock()
|
task.statusLock.Lock()
|
||||||
defer task.statusLock.Unlock()
|
defer task.statusLock.Unlock()
|
||||||
if task.Status != base.DownloadStatusRunning {
|
if task.Status != base.DownloadStatusRunning && !task.Uploading {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check if task is deleted
|
// check if task is deleted
|
||||||
if d.GetTask(task.ID) == nil {
|
if d.GetTask(task.ID) == nil || task.fetcher == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
current := task.fetcher.Progress().TotalDownloaded()
|
current := task.fetcher.Progress().TotalDownloaded()
|
||||||
|
tick := float64(d.cfg.RefreshInterval) / 1000
|
||||||
|
if task.Status == base.DownloadStatusRunning {
|
||||||
task.Progress.Used = task.timer.Used()
|
task.Progress.Used = task.timer.Used()
|
||||||
task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000)
|
task.Progress.Speed = task.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick)
|
||||||
task.Progress.Downloaded = current
|
task.Progress.Downloaded = current
|
||||||
|
}
|
||||||
|
if task.Uploading {
|
||||||
|
uploader := task.fetcher.(fetcher.Uploader)
|
||||||
|
currentUploaded := uploader.UploadedBytes()
|
||||||
|
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
|
||||||
|
task.Progress.Uploaded = currentUploaded
|
||||||
|
}
|
||||||
d.emit(EventKeyProgress, task)
|
d.emit(EventKeyProgress, task)
|
||||||
|
|
||||||
// store fetcher progress
|
// store fetcher progress
|
||||||
data, err := task.fetcherBuilder.Store(task.fetcher)
|
data, err := task.fetcherManager.Store(task.fetcher)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
||||||
return
|
return
|
||||||
@@ -211,21 +239,36 @@ func (d *Downloader) Setup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
|
func (d *Downloader) parseFm(url string) (fetcher.FetcherManager, error) {
|
||||||
schema := util.ParseSchema(url)
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
fetchBuilder, ok := d.fetcherBuilders[schema]
|
for _, filter := range fm.Filters() {
|
||||||
if ok {
|
if filter.Match(url) {
|
||||||
return fetchBuilder, nil
|
return fm, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, ErrUnSupportedProtocol
|
return nil, ErrUnSupportedProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fetcher) {
|
||||||
ctl := controller.NewController()
|
ctl := controller.NewController()
|
||||||
ctl.GetConfig = func(v any) bool {
|
ctl.GetConfig = func(v any) {
|
||||||
return d.getProtocolConfig(fetcher.Name(), v)
|
d.getProtocolConfig(fm.Name(), v)
|
||||||
|
}
|
||||||
|
// Get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||||
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
if requestProxy == nil {
|
||||||
|
return d.cfg.Proxy.ToHandler()
|
||||||
|
}
|
||||||
|
switch requestProxy.Mode {
|
||||||
|
case base.RequestProxyModeNone:
|
||||||
|
return nil
|
||||||
|
case base.RequestProxyModeCustom:
|
||||||
|
return requestProxy.ToHandler()
|
||||||
|
default:
|
||||||
|
return d.cfg.Proxy.ToHandler()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctl.ProxyConfig = d.cfg.Proxy
|
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +347,7 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
|
|||||||
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
|
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
|
||||||
taskIds := make([]string, 0)
|
taskIds := make([]string, 0)
|
||||||
for _, req := range reqs {
|
for _, req := range reqs {
|
||||||
taskId, err := d.CreateDirect(req, opts)
|
taskId, err := d.CreateDirect(req, opts.Clone())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -334,18 +377,10 @@ func (d *Downloader) Pause(id string) (err error) {
|
|||||||
return ErrTaskNotFound
|
return ErrTaskNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func() {
|
|
||||||
task.lock.Lock()
|
|
||||||
defer task.lock.Unlock()
|
|
||||||
|
|
||||||
if task.Status == base.DownloadStatusPause {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = d.doPause(task); err != nil {
|
if err = d.doPause(task); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.notifyRunning()
|
d.notifyRunning()
|
||||||
}()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,8 +499,10 @@ func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteIds := make([]string, 0)
|
deleteIds := make([]string, 0)
|
||||||
|
deleteTasksPtr := make([]*Task, 0)
|
||||||
for _, task := range deleteTasks {
|
for _, task := range deleteTasks {
|
||||||
deleteIds = append(deleteIds, task.ID)
|
deleteIds = append(deleteIds, task.ID)
|
||||||
|
deleteTasksPtr = append(deleteTasksPtr, task)
|
||||||
}
|
}
|
||||||
func() {
|
func() {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
@@ -487,7 +524,7 @@ func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err err
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, task := range deleteTasks {
|
for _, task := range deleteTasksPtr {
|
||||||
err = d.doDelete(task, force)
|
err = d.doDelete(task, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -503,6 +540,17 @@ func (d *Downloader) Stats(id string) (sr any, err error) {
|
|||||||
if task == nil {
|
if task == nil {
|
||||||
return sr, ErrTaskNotFound
|
return sr, ErrTaskNotFound
|
||||||
}
|
}
|
||||||
|
if task.fetcher == nil {
|
||||||
|
err = func() error {
|
||||||
|
task.statusLock.Lock()
|
||||||
|
defer task.statusLock.Unlock()
|
||||||
|
|
||||||
|
return d.restoreFetcher(task)
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
sr = task.fetcher.Stats()
|
sr = task.fetcher.Stats()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -548,10 +596,23 @@ func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
|||||||
|
|
||||||
func (d *Downloader) Close() error {
|
func (d *Downloader) Close() error {
|
||||||
d.closed.Store(true)
|
d.closed.Store(true)
|
||||||
if err := d.PauseAll(); err != nil {
|
|
||||||
return err
|
closeArr := []func() error{
|
||||||
|
d.PauseAll,
|
||||||
}
|
}
|
||||||
return d.storage.Close()
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
|
closeArr = append(closeArr, fm.Close)
|
||||||
|
}
|
||||||
|
closeArr = append(closeArr, d.storage.Close)
|
||||||
|
// Make sure all resources are released, if had error, return the last error
|
||||||
|
var lastErr error
|
||||||
|
for i, close := range closeArr {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("downloader close failed, index: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) Clear() error {
|
func (d *Downloader) Clear() error {
|
||||||
@@ -641,11 +702,49 @@ func (d *Downloader) getProtocolConfig(name string, v any) bool {
|
|||||||
|
|
||||||
// wait task done
|
// wait task done
|
||||||
func (d *Downloader) watch(task *Task) {
|
func (d *Downloader) watch(task *Task) {
|
||||||
|
if _, loaded := d.watchedTasks.LoadOrStore(task.ID, true); loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
d.watchedTasks.Delete(task.ID)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait task upload done
|
||||||
|
if task.Uploading {
|
||||||
|
if uploader, ok := task.fetcher.(fetcher.Uploader); ok {
|
||||||
|
go func() {
|
||||||
|
err := uploader.WaitUpload()
|
||||||
|
if err != nil {
|
||||||
|
d.Logger.Warn().Err(err).Msgf("task wait upload failed, task id: %s", task.ID)
|
||||||
|
}
|
||||||
|
d.lock.Lock()
|
||||||
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
// Check if the task is deleted
|
||||||
|
if d.GetTask(task.ID) != nil {
|
||||||
|
task.Uploading = false
|
||||||
|
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.Status == base.DownloadStatusDone {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := task.fetcher.Wait()
|
err := task.fetcher.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.doOnError(task, err)
|
d.doOnError(task, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When delete a not resolved task, need check if the task resource is nil
|
||||||
|
if task.Meta.Res == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
task.Progress.Used = task.timer.Used()
|
task.Progress.Used = task.timer.Used()
|
||||||
if task.Meta.Res.Size == 0 {
|
if task.Meta.Res.Size == 0 {
|
||||||
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
||||||
@@ -662,6 +761,7 @@ func (d *Downloader) watch(task *Task) {
|
|||||||
d.emit(EventKeyDone, task)
|
d.emit(EventKeyDone, task)
|
||||||
d.emit(EventKeyFinally, task, err)
|
d.emit(EventKeyFinally, task, err)
|
||||||
d.notifyRunning()
|
d.notifyRunning()
|
||||||
|
d.triggerOnDone(task)
|
||||||
|
|
||||||
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
|
||||||
downloadFilePath := task.Meta.SingleFilepath()
|
downloadFilePath := task.Meta.SingleFilepath()
|
||||||
@@ -695,15 +795,30 @@ func (d *Downloader) doOnError(task *Task, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) restoreFetcher(task *Task) error {
|
func (d *Downloader) restoreTask(task *Task) error {
|
||||||
if task.fetcher == nil {
|
if task.fetcher == nil {
|
||||||
fb, err := d.parseFb(task.Meta.Req.URL)
|
if err := d.restoreFetcher(task); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task.fetcherBuilder = fb
|
}
|
||||||
err = func() error {
|
go d.watch(task)
|
||||||
v, f := fb.Restore()
|
task.fetcher.Create(task.Meta.Opts)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) restoreFetcher(task *Task) error {
|
||||||
|
var fm fetcher.FetcherManager
|
||||||
|
for _, f := range d.cfg.FetchManagers {
|
||||||
|
if f.Name() == task.Protocol {
|
||||||
|
fm = f
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fm == nil {
|
||||||
|
return ErrUnSupportedProtocol
|
||||||
|
}
|
||||||
|
task.fetcherManager = fm
|
||||||
|
v, f := fm.Restore()
|
||||||
if v != nil {
|
if v != nil {
|
||||||
err := d.storage.Pop(bucketSave, task.ID, v)
|
err := d.storage.Pop(bucketSave, task.ID, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -711,30 +826,20 @@ func (d *Downloader) restoreFetcher(task *Task) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
task.fetcher = f(task.Meta, v)
|
task.fetcher = f(task.Meta, v)
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("deserialize fetcher failed, task id: %s", task.ID)
|
|
||||||
}
|
|
||||||
if task.fetcher == nil {
|
if task.fetcher == nil {
|
||||||
task.fetcher = fb.Build()
|
task.fetcher = task.fetcherManager.Build()
|
||||||
}
|
}
|
||||||
d.setupFetcher(task.fetcher)
|
d.setupFetcher(task.fetcherManager, task.fetcher)
|
||||||
if task.fetcher.Meta().Req == nil {
|
if task.fetcher.Meta().Req == nil {
|
||||||
task.fetcher.Meta().Req = task.Meta.Req
|
task.fetcher.Meta().Req = task.Meta.Req
|
||||||
}
|
}
|
||||||
if task.fetcher.Meta().Res == nil {
|
if task.fetcher.Meta().Res == nil {
|
||||||
task.fetcher.Meta().Res = task.Meta.Res
|
task.fetcher.Meta().Res = task.Meta.Res
|
||||||
}
|
}
|
||||||
go d.watch(task)
|
|
||||||
} else if task.Status == base.DownloadStatusError {
|
|
||||||
go d.watch(task)
|
|
||||||
}
|
|
||||||
task.fetcher.Create(task.Meta.Opts)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (taskId string, err error) {
|
func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId string, err error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
opts = &base.Options{}
|
opts = &base.Options{}
|
||||||
}
|
}
|
||||||
@@ -742,7 +847,7 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
|||||||
opts.SelectFiles = make([]int, 0)
|
opts.SelectFiles = make([]int, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := fetcher.Meta()
|
meta := f.Meta()
|
||||||
meta.Opts = opts
|
meta.Opts = opts
|
||||||
if opts.Path == "" {
|
if opts.Path == "" {
|
||||||
storeConfig, err := d.GetConfig()
|
storeConfig, err := d.GetConfig()
|
||||||
@@ -752,19 +857,20 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
|||||||
opts.Path = storeConfig.DownloadDir
|
opts.Path = storeConfig.DownloadDir
|
||||||
}
|
}
|
||||||
|
|
||||||
fb, err := d.parseFb(fetcher.Meta().Req.URL)
|
fm, err := d.parseFm(f.Meta().Req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
task := NewTask()
|
task := NewTask()
|
||||||
task.fetcherBuilder = fb
|
task.fetcherManager = fm
|
||||||
task.fetcher = fetcher
|
task.fetcher = f
|
||||||
task.Protocol = fetcher.Name()
|
task.Protocol = fm.Name()
|
||||||
task.Meta = fetcher.Meta()
|
task.Meta = f.Meta()
|
||||||
task.Progress = &Progress{}
|
task.Progress = &Progress{}
|
||||||
|
_, task.Uploading = f.(fetcher.Uploader)
|
||||||
initTask(task)
|
initTask(task)
|
||||||
if err = fetcher.Create(opts); err != nil {
|
if err = f.Create(opts); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
if err = d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||||
@@ -807,7 +913,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.restoreFetcher(task)
|
err = d.restoreTask(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
||||||
return
|
return
|
||||||
@@ -840,6 +946,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isCreate {
|
if isCreate {
|
||||||
|
if task.fetcherManager.AutoRename() {
|
||||||
d.checkDuplicateLock.Lock()
|
d.checkDuplicateLock.Lock()
|
||||||
defer d.checkDuplicateLock.Unlock()
|
defer d.checkDuplicateLock.Unlock()
|
||||||
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
|
||||||
@@ -861,16 +968,17 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
}
|
}
|
||||||
task.Meta.Opts.Name = newName
|
task.Meta.Opts.Name = newName
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
|
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
task.Progress.Speed = 0
|
task.Progress.Speed = 0
|
||||||
task.timer.Start()
|
task.timer.Start()
|
||||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
if err := task.fetcher.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := task.fetcher.Start(); err != nil {
|
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.emit(EventKeyStart, task)
|
d.emit(EventKeyStart, task)
|
||||||
@@ -940,12 +1048,12 @@ func logPanic(logDir string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||||
fb, err := d.parseFb(url)
|
fm, err := d.parseFm(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fetcher := fb.Build()
|
fetcher := fm.Build()
|
||||||
d.setupFetcher(fetcher)
|
d.setupFetcher(fm, fetcher)
|
||||||
return fetcher, nil
|
return fetcher, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -955,6 +1063,7 @@ func initTask(task *Task) {
|
|||||||
task.statusLock = &sync.Mutex{}
|
task.statusLock = &sync.Mutex{}
|
||||||
task.lock = &sync.Mutex{}
|
task.lock = &sync.Mutex{}
|
||||||
task.speedArr = make([]int64, 0)
|
task.speedArr = make([]int64, 0)
|
||||||
|
task.uploadSpeedArr = make([]int64, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultDownloader = NewDownloader(nil)
|
var defaultDownloader = NewDownloader(nil)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -92,15 +94,33 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
if err := downloader.Setup(); err != nil {
|
if err := downloader.Setup(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer downloader.Clear()
|
defer func() {
|
||||||
|
downloader.DeleteByStatues(nil, true)
|
||||||
|
downloader.Clear()
|
||||||
|
}()
|
||||||
|
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.Request, 0)
|
||||||
for i := 0; i < 6; i++ {
|
fileNames := make([]string, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
}
|
}
|
||||||
reqs = append(reqs, req)
|
reqs = append(reqs, req)
|
||||||
|
if i == 0 {
|
||||||
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
|
} else {
|
||||||
|
arr := strings.Split(test.DownloadName, ".")
|
||||||
|
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(reqs))
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
||||||
Path: test.Dir,
|
Path: test.Dir,
|
||||||
@@ -113,29 +133,47 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
tasks := downloader.GetTasks()
|
tasks := downloader.GetTasks()
|
||||||
if len(tasks) != len(reqs) {
|
if len(tasks) != len(reqs) {
|
||||||
t.Errorf("CreateDirectBatch() got = %v, want %v", len(tasks), len(reqs))
|
t.Errorf("CreateDirectBatch() task got = %v, want %v", len(tasks), len(reqs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nameMatchMap := make(map[string]bool)
|
||||||
|
for _, name := range fileNames {
|
||||||
|
if _, err := os.Stat(test.Dir + "/" + name); os.IsNotExist(err) {
|
||||||
|
t.Errorf("CreateDirectBatch() file not exist: %v", name)
|
||||||
|
}
|
||||||
|
for _, task := range tasks {
|
||||||
|
if name == task.Meta.Opts.Name {
|
||||||
|
nameMatchMap[task.Meta.Opts.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nameMatchMap) != len(reqs) {
|
||||||
|
t.Errorf("CreateDirectBatch() names got = %v, want %v", len(nameMatchMap), len(reqs))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||||
// No proxy
|
// No proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
// Disable proxy
|
// Disable proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Enable = false
|
proxyCfg.Enable = false
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Enable system proxy but not set proxy environment variable
|
// Enable system proxy but not set proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Enable proxy but error proxy environment variable
|
// Enable proxy but error proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
||||||
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
@@ -146,33 +184,50 @@ func TestDownloader_CreateWithProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Enable system proxy and set proxy environment variable
|
// Enable system proxy and set proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
||||||
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Invalid proxy scheme
|
// Invalid proxy scheme
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Scheme = ""
|
proxyCfg.Scheme = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Invalid proxy host
|
// Invalid proxy host
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Host = ""
|
proxyCfg.Host = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Use proxy without auth
|
// Use proxy without auth
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Use proxy with auth
|
// Use proxy with auth
|
||||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
|
// Request proxy mode follow
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeFollow
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode none
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeNone
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode custom
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
||||||
usr, pwd := "", ""
|
usr, pwd := "", ""
|
||||||
if auth {
|
if auth {
|
||||||
usr, pwd = "admin", "123"
|
usr, pwd = "admin", "123"
|
||||||
@@ -185,17 +240,29 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer downloader.Clear()
|
defer downloader.Clear()
|
||||||
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&base.DownloaderProxyConfig{
|
globalProxyCfg := &base.DownloaderProxyConfig{
|
||||||
Enable: true,
|
Enable: true,
|
||||||
Scheme: "socks5",
|
Scheme: "socks5",
|
||||||
Host: proxyListener.Addr().String(),
|
Host: proxyListener.Addr().String(),
|
||||||
Usr: usr,
|
Usr: usr,
|
||||||
Pwd: pwd,
|
Pwd: pwd,
|
||||||
})
|
}
|
||||||
|
if buildProxyConfig != nil {
|
||||||
|
globalProxyCfg = buildProxyConfig(globalProxyCfg)
|
||||||
|
}
|
||||||
|
downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg
|
||||||
|
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: test.ExternalDownloadUrl,
|
URL: test.ExternalDownloadUrl,
|
||||||
}
|
}
|
||||||
|
if buildReqProxy != nil {
|
||||||
|
req.Proxy = buildReqProxy(&base.RequestProxy{
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
rr, err := downloader.Resolve(req)
|
rr, err := downloader.Resolve(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errHandler == nil {
|
if errHandler == nil {
|
||||||
@@ -342,8 +409,8 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
|||||||
|
|
||||||
var httpCfg map[string]any
|
var httpCfg map[string]any
|
||||||
exits := downloader.getProtocolConfig("http", &httpCfg)
|
exits := downloader.getProtocolConfig("http", &httpCfg)
|
||||||
if exits {
|
if !exits {
|
||||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
t.Errorf("getProtocolConfig() got = %v, want %v", exits, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
storeCfg := &base.DownloaderStoreConfig{
|
storeCfg := &base.DownloaderStoreConfig{
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (e *Engine) await(value any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) Close() {
|
func (e *Engine) Close() {
|
||||||
e.loop.Stop()
|
e.loop.StopNoWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -178,7 +178,15 @@ func resolveResult(value goja.Value) (any, error) {
|
|||||||
if err, ok := p.Result().Export().(error); ok {
|
if err, ok := p.Result().Export().(error); ok {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New(p.Result().String())
|
stack := p.Result().String()
|
||||||
|
result := p.Result()
|
||||||
|
if ro, ok := result.(*goja.Object); ok {
|
||||||
|
stackVal := ro.Get("stack")
|
||||||
|
if stackVal != nil && stackVal.String() != "" {
|
||||||
|
stack = stackVal.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New(stack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/test"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
|
||||||
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,6 +13,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPolyfill(t *testing.T) {
|
func TestPolyfill(t *testing.T) {
|
||||||
@@ -26,6 +27,8 @@ func TestPolyfill(t *testing.T) {
|
|||||||
doTestPolyfill(t, "XMLHttpRequest")
|
doTestPolyfill(t, "XMLHttpRequest")
|
||||||
doTestPolyfill(t, "Blob")
|
doTestPolyfill(t, "Blob")
|
||||||
doTestPolyfill(t, "FormData")
|
doTestPolyfill(t, "FormData")
|
||||||
|
doTestPolyfill(t, "TextDecoder")
|
||||||
|
doTestPolyfill(t, "TextEncoder")
|
||||||
doTestPolyfill(t, "fetch")
|
doTestPolyfill(t, "fetch")
|
||||||
doTestPolyfill(t, "__gopeed_create_vm")
|
doTestPolyfill(t, "__gopeed_create_vm")
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,44 @@ async function testOctetStream(file){
|
|||||||
return await resp.text();
|
return await resp.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testRedirect() {
|
||||||
|
const url = host + '/redirect?num=3'
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
redirect: 'error',
|
||||||
|
}).then(()=>reject())
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
redirect: 'follow',
|
||||||
|
}).then((res) =>res.headers.has('location') && reject()).catch(() => reject())
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
redirect: 'manual',
|
||||||
|
}).then((res) => {
|
||||||
|
const location = res.headers.get('location');
|
||||||
|
location ? resolve(location) : reject()
|
||||||
|
}).catch(() => reject())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testResponseUrl() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', host+'/redirect?num=3');
|
||||||
|
xhr.onload = function(){
|
||||||
|
if (xhr.responseURL.includes('/redirect?num=0')){
|
||||||
|
resolve();
|
||||||
|
}else{
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function testFormData(file){
|
async function testFormData(file){
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('name', 'test');
|
formData.append('name', 'test');
|
||||||
@@ -196,6 +237,20 @@ function testTimeout(){
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
t.Run("testRedirect", func(t *testing.T) {
|
||||||
|
_, err := callTestFun(engine, "testRedirect")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("testResponseUrl", func(t *testing.T) {
|
||||||
|
_, err = callTestFun(engine, "testResponseUrl")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
jsFile, goFile, md5 := buildFile(t, engine.Runtime)
|
jsFile, goFile, md5 := buildFile(t, engine.Runtime)
|
||||||
result, err = callTestFun(engine, "testFormData", jsFile)
|
result, err = callTestFun(engine, "testFormData", jsFile)
|
||||||
@@ -405,6 +460,16 @@ func startServer() net.Listener {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
num := r.URL.Query().Get("num")
|
||||||
|
n, _ := strconv.Atoi(num)
|
||||||
|
if n > 0 {
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/redirect?num=%d", n-1), http.StatusFound)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
})
|
||||||
server.Handler = mux
|
server.Handler = mux
|
||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
return listener
|
return listener
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ package xhr
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
@@ -14,6 +10,11 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,6 +26,12 @@ const (
|
|||||||
eventTimeout = "timeout"
|
eventTimeout = "timeout"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
redirectError = "error"
|
||||||
|
redirectFollow = "follow"
|
||||||
|
redirectManual = "manual"
|
||||||
|
)
|
||||||
|
|
||||||
type ProgressEvent struct {
|
type ProgressEvent struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
LengthComputable bool `json:"lengthComputable"`
|
LengthComputable bool `json:"lengthComputable"`
|
||||||
@@ -124,6 +131,7 @@ type XMLHttpRequest struct {
|
|||||||
aborted bool
|
aborted bool
|
||||||
proxyHandler func(r *http.Request) (*url.URL, error)
|
proxyHandler func(r *http.Request) (*url.URL, error)
|
||||||
|
|
||||||
|
WithCredentials bool `json:"withCredentials"`
|
||||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
ReadyState int `json:"readyState"`
|
ReadyState int `json:"readyState"`
|
||||||
@@ -131,6 +139,13 @@ type XMLHttpRequest struct {
|
|||||||
StatusText string `json:"statusText"`
|
StatusText string `json:"statusText"`
|
||||||
Response string `json:"response"`
|
Response string `json:"response"`
|
||||||
ResponseText string `json:"responseText"`
|
ResponseText string `json:"responseText"`
|
||||||
|
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseURL
|
||||||
|
// https://xhr.spec.whatwg.org/#the-responseurl-attribute
|
||||||
|
ResponseUrl string `json:"responseURL"`
|
||||||
|
// extend fetch redirect
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#redirect
|
||||||
|
// https://fetch.spec.whatwg.org/#concept-request-redirect-mode
|
||||||
|
Redirect string `json:"redirect"`
|
||||||
*EventProp
|
*EventProp
|
||||||
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
|
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
|
||||||
}
|
}
|
||||||
@@ -154,31 +169,33 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
var (
|
var (
|
||||||
contentType string
|
contentType string
|
||||||
contentLength int64
|
contentLength int64
|
||||||
|
isStringBody bool
|
||||||
)
|
)
|
||||||
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
||||||
} else {
|
} else {
|
||||||
switch d.(type) {
|
switch v := d.(type) {
|
||||||
case string:
|
case string:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string)))
|
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
|
||||||
contentType = "text/plain;charset=UTF-8"
|
contentType = "text/plain;charset=UTF-8"
|
||||||
contentLength = int64(len(d.(string)))
|
contentLength = int64(len(v))
|
||||||
|
isStringBody = true
|
||||||
case *file.File:
|
case *file.File:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader)
|
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
contentLength = d.(*file.File).Size
|
contentLength = v.Size
|
||||||
case *formdata.FormData:
|
case *formdata.FormData:
|
||||||
pr, pw := io.Pipe()
|
pr, pw := io.Pipe()
|
||||||
mw := NewMultipart(pw)
|
mw := NewMultipart(pw)
|
||||||
for _, e := range d.(*formdata.FormData).Entries() {
|
for _, e := range v.Entries() {
|
||||||
arr := e.([]any)
|
arr := e.([]any)
|
||||||
k := arr[0].(string)
|
k := arr[0].(string)
|
||||||
v := arr[1]
|
v := arr[1]
|
||||||
switch v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
mw.WriteField(k, v.(string))
|
mw.WriteField(k, v)
|
||||||
case *file.File:
|
case *file.File:
|
||||||
mw.WriteFile(k, v.(*file.File))
|
mw.WriteFile(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
@@ -196,7 +213,8 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Header = xhr.requestHeaders
|
req.Header = xhr.requestHeaders
|
||||||
if contentType != "" {
|
// Only string body can specify Content-Type header by user
|
||||||
|
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
|
||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
if contentLength > 0 {
|
if contentLength > 0 {
|
||||||
@@ -208,6 +226,18 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if xhr.Redirect == redirectManual {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}
|
||||||
|
if xhr.Redirect == redirectError {
|
||||||
|
return errors.New("redirect failed")
|
||||||
|
}
|
||||||
|
if len(via) > 20 {
|
||||||
|
return errors.New("too many redirects")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -229,6 +259,11 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.Upload.callOnload()
|
xhr.Upload.callOnload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
responseUrl := resp.Request.URL
|
||||||
|
responseUrl.Fragment = ""
|
||||||
|
xhr.ResponseUrl = responseUrl.String()
|
||||||
|
|
||||||
xhr.responseHeaders = resp.Header
|
xhr.responseHeaders = resp.Header
|
||||||
xhr.Status = resp.StatusCode
|
xhr.Status = resp.StatusCode
|
||||||
xhr.StatusText = resp.Status
|
xhr.StatusText = resp.Status
|
||||||
@@ -384,10 +419,9 @@ func (w *multipartWrapper) Size() int64 {
|
|||||||
w.statWriter.Close()
|
w.statWriter.Close()
|
||||||
size := int64(w.statBuffer.Len())
|
size := int64(w.statBuffer.Len())
|
||||||
for _, v := range w.fields {
|
for _, v := range w.fields {
|
||||||
switch v.(type) {
|
switch v := v.(type) {
|
||||||
case *file.File:
|
case *file.File:
|
||||||
f := v.(*file.File)
|
size += v.Size
|
||||||
size += f.Size
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return size
|
return size
|
||||||
@@ -395,18 +429,17 @@ func (w *multipartWrapper) Size() int64 {
|
|||||||
|
|
||||||
func (w *multipartWrapper) Send() error {
|
func (w *multipartWrapper) Send() error {
|
||||||
for k, v := range w.fields {
|
for k, v := range w.fields {
|
||||||
switch v.(type) {
|
switch v := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
if err := w.writer.WriteField(k, v.(string)); err != nil {
|
if err := w.writer.WriteField(k, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case *file.File:
|
case *file.File:
|
||||||
f := v.(*file.File)
|
fw, err := w.writer.CreateFormFile(k, v.Name)
|
||||||
fw, err := w.writer.CreateFormFile(k, f.Name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err = io.Copy(fw, f); err != nil {
|
if _, err = io.Copy(fw, v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e<r;){var u=t.charCodeAt(e++);if(u>=55296&&u<=56319){if(e<r){var f=t.charCodeAt(e);56320==(64512&f)&&(++e,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o<e;){var n,i,a,s,u=t[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p<c;)h+=String.fromCharCode.apply(String,r.slice(p,p+=4096));return h};function b(){var e=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;e&&r&&(XMLHttpRequest.prototype.send=function(t){t instanceof Blob?(this.setRequestHeader("Content-Type",t.type),r.call(this,t)):r.call(this,t)});try{new File([],""),t.File=i.File,t.FileReader=i.FileReader}catch(e){try{t.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(e){t.File=function(t,e,r){var o=new Blob(t,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=e.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),t.Blob=f?i.Blob:p):l?(b(),t.Blob=h):function(){function e(t){for(var e=new Array(t.byteLength),r=new Uint8Array(t),o=e.length;o--;)e[o]=r[o];return e}function o(t){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<t.length;o+=3){var n=t[o],i=o+1<t.length,a=i?t[o+1]:0,s=o+2<t.length,u=s?t[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o<n;o++){var a=t[o];a instanceof h?t[o]=a._buffer:"string"==typeof a?t[o]=y(a):"DataView"===u(s=a)||f(i.DataView,s)?t[o]=e(a.buffer):c(a)?t[o]=e(a):t[o]=y(String(a))}var s;this._buffer=i.Uint8Array?function(t){for(var e=0,r=t.length;r--;)e+=t[r].length;for(var o=new Uint8Array(e),n=0,i=0;i<t.length;i++){var a=t[i];o.set(a,n),n+=a.byteLength||a.length}return o}(t):[].concat.apply([],t),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
(()=>{var e={725:function(e,t,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(e){"use strict";var t=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(e,t){return(t=document.createElement("a")).href=e,t},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=t&&t.prototype.append&&t.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(e){}function c(e){return e.map((function(e){if(e.buffer instanceof ArrayBuffer){var t=e.buffer;if(e.byteLength!==t.byteLength){var r=new Uint8Array(e.byteLength);r.set(new Uint8Array(t,e.byteOffset,e.byteLength)),t=r.buffer}return t}return e}))}function h(e,r){r=r||{};var o=new t;return c(e).forEach((function(e){o.append(e)})),r.type?o.getBlob(r.type):o.getBlob()}function d(e,t){return new o(c(e),t||{})}i.Blob&&(h.prototype=Blob.prototype,d.prototype=Blob.prototype);var p="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(e){for(var t=0,r=e.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);t<r;){var u=e.charCodeAt(t++);if(u>=55296&&u<=56319){if(t<r){var f=e.charCodeAt(t);56320==(64512&f)&&(++t,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+t/e.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},y="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(e){for(var t=e.length,r=[],o=0;o<t;){var n,i,a,s,u=e[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=t)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=e[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=e[o+1],i=e[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=e[o+1],i=e[o+2],a=e[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",d=0;d<c;)h+=String.fromCharCode.apply(String,r.slice(d,d+=4096));return h};function b(){var t=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;t&&r&&(XMLHttpRequest.prototype.send=function(e){e instanceof Blob?(this.setRequestHeader("Content-Type",e.type),r.call(this,e)):r.call(this,e)});try{new File([],""),e.File=i.File,e.FileReader=i.FileReader}catch(t){try{e.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(t){e.File=function(e,t,r){var o=new Blob(e,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=t.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),e.Blob=f?i.Blob:d):l?(b(),e.Blob=h):function(){function t(e){for(var t=new Array(e.byteLength),r=new Uint8Array(e),o=t.length;o--;)t[o]=r[o];return t}function o(e){for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<e.length;o+=3){var n=e[o],i=o+1<e.length,a=i?e[o+1]:0,s=o+2<e.length,u=s?e[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(t[f],t[l],t[c],t[h])}return r.join("")}var s=Object.create||function(e){function t(){}return t.prototype=e,new t};function u(e){return Object.prototype.toString.call(e).slice(8,-1)}function f(e,t){return"object"==typeof e&&Object.prototype.isPrototypeOf.call(e.prototype,t)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(e){return t=l,r=u(e),-1!==t.indexOf(r)||f(i.ArrayBuffer,e);var t,r}function h(e,r){r=null==r?{}:r;for(var o=0,n=(e=e?e.slice():[]).length;o<n;o++){var a=e[o];a instanceof h?e[o]=a._buffer:"string"==typeof a?e[o]=p(a):"DataView"===u(s=a)||f(i.DataView,s)?e[o]=t(a.buffer):c(a)?e[o]=t(a):e[o]=p(String(a))}var s;this._buffer=i.Uint8Array?function(e){for(var t=0,r=e.length;r--;)t+=e[r].length;for(var o=new Uint8Array(t),n=0,i=0;i<e.length;i++){var a=e[i];o.set(a,n),n+=a.byteLength||a.length}return o}(e):[].concat.apply([],e),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
|
||||||
+1169
-15
File diff suppressed because it is too large.
Load diff
@@ -7,16 +7,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "webpack --mode production --watch",
|
"dev": "webpack --mode production --watch",
|
||||||
"build": "webpack --mode production"
|
"build": "webpack --mode production",
|
||||||
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.75.0",
|
||||||
"webpack-cli": "^5.0.1"
|
"webpack-cli": "^5.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"blob-polyfill": "^7.0.20220408",
|
"blob-polyfill": "^7.0.20220408",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"fastestsmallesttextencoderdecoder": "^1.0.22",
|
||||||
|
"whatwg-fetch": "^3.6.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
diff --git a/node_modules/whatwg-fetch/dist/fetch.umd.js b/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||||
|
index 7a0d852..604691e 100644
|
||||||
|
--- a/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||||
|
+++ b/node_modules/whatwg-fetch/dist/fetch.umd.js
|
||||||
|
@@ -394,6 +394,7 @@
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
this.referrer = null;
|
||||||
|
+ this.redirect = options.redirect || 'follow'
|
||||||
|
|
||||||
|
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||||
|
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||||
|
@@ -606,6 +607,10 @@
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ if ('redirect' in xhr) {
|
||||||
|
+ xhr.redirect = request.redirect
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||||
|
var names = [];
|
||||||
|
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||||
|
diff --git a/node_modules/whatwg-fetch/fetch.js b/node_modules/whatwg-fetch/fetch.js
|
||||||
|
index f39a983..d1fc903 100644
|
||||||
|
--- a/node_modules/whatwg-fetch/fetch.js
|
||||||
|
+++ b/node_modules/whatwg-fetch/fetch.js
|
||||||
|
@@ -388,6 +388,7 @@ export function Request(input, options) {
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
this.referrer = null
|
||||||
|
+ this.redirect = options.redirect || 'follow'
|
||||||
|
|
||||||
|
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||||
|
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||||
|
@@ -600,6 +601,10 @@ export function fetch(input, init) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ if ('redirect' in xhr) {
|
||||||
|
+ xhr.redirect = request.redirect
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||||
|
var names = [];
|
||||||
|
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import "./blob/index.js"
|
import "./blob/index.js"
|
||||||
import "./crypto/index.js"
|
import "./crypto/index.js"
|
||||||
|
// polyfill TextEncoder
|
||||||
|
import 'fastestsmallesttextencoderdecoder';
|
||||||
import "./fetch/index.js"
|
import "./fetch/index.js"
|
||||||
@@ -38,7 +38,7 @@ const (
|
|||||||
EventOnResolve ActivationEvent = "onResolve"
|
EventOnResolve ActivationEvent = "onResolve"
|
||||||
EventOnStart ActivationEvent = "onStart"
|
EventOnStart ActivationEvent = "onStart"
|
||||||
EventOnError ActivationEvent = "onError"
|
EventOnError ActivationEvent = "onError"
|
||||||
//EventOnDone ActivationEvent = "onDone"
|
EventOnDone ActivationEvent = "onDone"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
||||||
@@ -155,7 +155,13 @@ func (d *Downloader) DeleteExtension(identity string) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d.storage.Delete(bucketExtension, identity)
|
if err = d.storage.Delete(bucketExtension, identity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = d.storage.Delete(bucketExtensionStorage, identity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) GetExtensions() []*Extension {
|
func (d *Downloader) GetExtensions() []*Extension {
|
||||||
@@ -298,6 +304,17 @@ func (d *Downloader) triggerOnError(task *Task, err error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) triggerOnDone(task *Task) {
|
||||||
|
doTrigger(d,
|
||||||
|
EventOnDone,
|
||||||
|
task.Meta.Req,
|
||||||
|
&OnErrorContext{
|
||||||
|
Task: NewExtensionTask(d, task),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
||||||
// init extension global object
|
// init extension global object
|
||||||
gopeed := &Instance{
|
gopeed := &Instance{
|
||||||
@@ -605,9 +622,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
|
|||||||
h.register(EventOnError, fn)
|
h.register(EventOnError, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (h InstanceEvents) OnDone(fn goja.Callable) {
|
func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||||
// h.register(HookEventOnDone, fn)
|
h.register(EventOnDone, fn)
|
||||||
//}
|
}
|
||||||
|
|
||||||
type ExtensionInfo struct {
|
type ExtensionInfo struct {
|
||||||
Identity string `json:"identity"`
|
Identity string `json:"identity"`
|
||||||
@@ -681,6 +698,10 @@ type OnErrorContext struct {
|
|||||||
Error error `json:"error"`
|
Error error `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnDoneContext struct {
|
||||||
|
Task *Task `json:"task"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExtensionTask struct {
|
type ExtensionTask struct {
|
||||||
*Task
|
*Task
|
||||||
|
|
||||||
@@ -688,8 +709,12 @@ type ExtensionTask struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
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{
|
return &ExtensionTask{
|
||||||
Task: task.clone(),
|
Task: newTask,
|
||||||
download: download,
|
download: download,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
|||||||
select {
|
select {
|
||||||
case err = <-errCh:
|
case err = <-errCh:
|
||||||
break
|
break
|
||||||
case <-time.After(time.Second * 30):
|
case <-time.After(time.Second * 10):
|
||||||
err = errors.New("timeout")
|
err = errors.New("timeout")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,8 +247,45 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// extension on error modify url and continue download
|
// extension on error modify url and continue download
|
||||||
task := downloader.GetTask(id)
|
task := downloader.GetTask(id)
|
||||||
if task.Meta.Req.URL != "https://github.com" {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_Extension_OnDone(t *testing.T) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_done", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyFinally {
|
||||||
|
errCh <- event.Err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
id, err := downloader.CreateDirect(&base.Request{
|
||||||
|
URL: "https://github.com",
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-errCh:
|
||||||
|
break
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
err = errors.New("timeout")
|
||||||
|
}
|
||||||
|
// wait for script execution
|
||||||
|
time.Sleep(time.Millisecond * 3000)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("extension on done download error: " + err.Error())
|
||||||
|
}
|
||||||
|
// extension on error modify url and continue download
|
||||||
|
task := downloader.GetTask(id)
|
||||||
|
if task.Meta.Req.Labels["modified"] != "true" {
|
||||||
|
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
|
||||||
}
|
}
|
||||||
if task.Status != base.DownloadStatusDone {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except status is done, actual: %s", task.Status)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
|||||||
+58
-21
@@ -1,6 +1,7 @@
|
|||||||
package download
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
||||||
@@ -19,19 +20,21 @@ type ResolveResult struct {
|
|||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||||
Status base.Status `json:"status"`
|
Status base.Status `json:"status"`
|
||||||
Protocol string `json:"protocol"`
|
Uploading bool `json:"uploading"`
|
||||||
Progress *Progress `json:"progress"`
|
Progress *Progress `json:"progress"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
fetcherBuilder fetcher.FetcherBuilder
|
fetcherManager fetcher.FetcherManager
|
||||||
fetcher fetcher.Fetcher
|
fetcher fetcher.Fetcher
|
||||||
timer *util.Timer
|
timer *util.Timer
|
||||||
statusLock *sync.Mutex
|
statusLock *sync.Mutex
|
||||||
lock *sync.Mutex
|
lock *sync.Mutex
|
||||||
speedArr []int64
|
speedArr []int64
|
||||||
|
uploadSpeedArr []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTask() *Task {
|
func NewTask() *Task {
|
||||||
@@ -47,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) {
|
func (t *Task) updateStatus(status base.Status) {
|
||||||
t.UpdatedAt = time.Now()
|
t.UpdatedAt = time.Now()
|
||||||
t.Status = status
|
t.Status = status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) clone() *Task {
|
func (t *Task) clone() *Task {
|
||||||
return &Task{
|
return util.DeepClone(t)
|
||||||
ID: t.ID,
|
|
||||||
Meta: t.Meta,
|
|
||||||
Status: t.Status,
|
|
||||||
Progress: t.Progress,
|
|
||||||
CreatedAt: t.CreatedAt,
|
|
||||||
UpdatedAt: t.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
|
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
||||||
t.speedArr = append(t.speedArr, downloaded)
|
speedArr = append(speedArr, downloaded)
|
||||||
if len(t.speedArr) > 6 {
|
if len(speedArr) > 6 {
|
||||||
t.speedArr = t.speedArr[1:]
|
speedArr = speedArr[1:]
|
||||||
}
|
}
|
||||||
var total int64
|
var total int64
|
||||||
for _, v := range t.speedArr {
|
for _, v := range speedArr {
|
||||||
total += v
|
total += v
|
||||||
}
|
}
|
||||||
return int64(float64(total) / float64(len(t.speedArr)) / usedTime)
|
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloaderConfig struct {
|
type DownloaderConfig struct {
|
||||||
Controller *controller.Controller
|
Controller *controller.Controller
|
||||||
FetchBuilders []fetcher.FetcherBuilder
|
FetchManagers []fetcher.FetcherManager
|
||||||
|
|
||||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||||
Storage Storage
|
Storage Storage
|
||||||
@@ -92,10 +129,10 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
|||||||
if cfg.Controller == nil {
|
if cfg.Controller == nil {
|
||||||
cfg.Controller = controller.NewController()
|
cfg.Controller = controller.NewController()
|
||||||
}
|
}
|
||||||
if len(cfg.FetchBuilders) == 0 {
|
if len(cfg.FetchManagers) == 0 {
|
||||||
cfg.FetchBuilders = []fetcher.FetcherBuilder{
|
cfg.FetchManagers = []fetcher.FetcherManager{
|
||||||
new(http.FetcherBuilder),
|
new(http.FetcherManager),
|
||||||
new(bt.FetcherBuilder),
|
new(bt.FetcherManager),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.RefreshInterval == 0 {
|
if cfg.RefreshInterval == 0 {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
gopeed.events.onDone(async function (ctx) {
|
||||||
|
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||||
|
ctx.task.meta.req.labels['modified'] = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "on-done",
|
||||||
|
"title": "gopeed extension on done event test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onDone",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ gopeed.events.onError(async function (ctx) {
|
|||||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||||
gopeed.logger.info("error", ctx.error);
|
gopeed.logger.info("error", ctx.error);
|
||||||
ctx.task.meta.req.url = "https://github.com";
|
ctx.task.meta.req.url = "https://github.com";
|
||||||
ctx.task.pause();
|
|
||||||
ctx.task.continue();
|
ctx.task.continue();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4,11 +4,16 @@ type ReqExtra struct {
|
|||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats for download
|
// Stats for torrent
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
// bt stats
|
|
||||||
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
|
||||||
TotalPeers int `json:"totalPeers"`
|
TotalPeers int `json:"totalPeers"`
|
||||||
ActivePeers int `json:"activePeers"`
|
ActivePeers int `json:"activePeers"`
|
||||||
ConnectedSeeders int `json:"connectedSeeders"`
|
ConnectedSeeders int `json:"connectedSeeders"`
|
||||||
|
// Total seed bytes
|
||||||
|
SeedBytes int64 `json:"seedBytes"`
|
||||||
|
// Seed ratio
|
||||||
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
|
// Total seed time
|
||||||
|
SeedTime int64 `json:"seedTime"`
|
||||||
}
|
}
|
||||||
+1
-1
@@ -17,8 +17,8 @@ func Info(w http.ResponseWriter, r *http.Request) {
|
|||||||
"runtime": runtime.Version(),
|
"runtime": runtime.Version(),
|
||||||
"os": runtime.GOOS,
|
"os": runtime.GOOS,
|
||||||
"arch": runtime.GOARCH,
|
"arch": runtime.GOARCH,
|
||||||
|
"inDocker": base.InDocker == "true",
|
||||||
}
|
}
|
||||||
info["inDocker"] = base.InDocker == "true"
|
|
||||||
WriteJson(w, model.NewOkResult(info))
|
WriteJson(w, model.NewOkResult(info))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ type StartConfig struct {
|
|||||||
Storage Storage `json:"storage"`
|
Storage Storage `json:"storage"`
|
||||||
StorageDir string `json:"storageDir"`
|
StorageDir string `json:"storageDir"`
|
||||||
ApiToken string `json:"apiToken"`
|
ApiToken string `json:"apiToken"`
|
||||||
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
Loaded 50 of 100 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user