mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 03:43:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||||
)
|
)
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
GetConfig func(v any) bool
|
GetConfig func(v any)
|
||||||
ProxyConfig *base.DownloaderProxyConfig
|
ProxyConfig *base.DownloaderProxyConfig
|
||||||
FileController
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
@@ -22,10 +22,8 @@ type DefaultFileController struct {
|
|||||||
|
|
||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
|
GetConfig: func(v any) {},
|
||||||
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,53 @@ 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
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+240
-67
@@ -2,6 +2,7 @@ package bt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
@@ -22,6 +23,8 @@ var (
|
|||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
torrentDirMap = make(map[string]string)
|
torrentDirMap = make(map[string]string)
|
||||||
ftMap = make(map[string]*fileTorrentImpl)
|
ftMap = make(map[string]*fileTorrentImpl)
|
||||||
|
closeCtx context.Context
|
||||||
|
closeFunc func()
|
||||||
)
|
)
|
||||||
|
|
||||||
type Fetcher struct {
|
type Fetcher struct {
|
||||||
@@ -30,15 +33,13 @@ type Fetcher struct {
|
|||||||
|
|
||||||
torrent *torrent.Torrent
|
torrent *torrent.Torrent
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
|
data *fetcherData
|
||||||
|
|
||||||
torrentReady atomic.Bool
|
torrentReady atomic.Bool
|
||||||
torrentDrop atomic.Bool
|
torrentUpload atomic.Bool
|
||||||
create atomic.Bool
|
torrentDropCtx context.Context
|
||||||
progress fetcher.Progress
|
torrentDropFunc func()
|
||||||
}
|
uploadDoneCh chan any
|
||||||
|
|
||||||
func (f *Fetcher) Name() string {
|
|
||||||
return "bt"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||||
@@ -46,13 +47,12 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
|||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
exist := f.ctl.GetConfig(&f.config)
|
if f.data == nil {
|
||||||
if !exist {
|
f.data = &fetcherData{}
|
||||||
f.config = &config{
|
|
||||||
ListenPort: 0,
|
|
||||||
Trackers: []string{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
f.uploadDoneCh = make(chan any, 1)
|
||||||
|
f.torrentDropCtx, f.torrentDropFunc = context.WithCancel(context.Background())
|
||||||
|
f.ctl.GetConfig(&f.config)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +63,12 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
if client != nil {
|
if client != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if closeCtx == nil {
|
||||||
|
closeCtx, closeFunc = context.WithCancel(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
cfg := torrent.NewDefaultClientConfig()
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.Seed = true
|
||||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||||
cfg.ListenPort = f.config.ListenPort
|
cfg.ListenPort = f.config.ListenPort
|
||||||
@@ -78,12 +82,22 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
ftMap[infoHash.String()] = ft
|
ftMap[infoHash.String()] = ft
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
dnsResolver := &DnsCacheResolver{RefreshTimeout: 5 * time.Minute}
|
||||||
|
cfg.TrackerDialContext = dnsResolver.DialContext
|
||||||
client, err = torrent.NewClient(cfg)
|
client, err = torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCtx, closeFunc = context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
dnsResolver.Run(closeCtx)
|
||||||
|
}()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
if err := f.addTorrent(req); err != nil {
|
if err := f.addTorrent(req, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.updateRes()
|
f.updateRes()
|
||||||
@@ -92,7 +106,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
f.create.Store(true)
|
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
if f.meta.Res != nil {
|
if f.meta.Res != nil {
|
||||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
||||||
@@ -102,7 +115,7 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
|
|||||||
|
|
||||||
func (f *Fetcher) Start() (err error) {
|
func (f *Fetcher) Start() (err error) {
|
||||||
if !f.torrentReady.Load() {
|
if !f.torrentReady.Load() {
|
||||||
if err = f.addTorrent(f.meta.Req); err != nil {
|
if err = f.addTorrent(f.meta.Req, false); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,13 +124,15 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
}
|
}
|
||||||
files := f.torrent.Files()
|
files := f.torrent.Files()
|
||||||
// If the user does not specify the file to download, all files will be downloaded by default
|
// If the user does not specify the file to download, all files will be downloaded by default
|
||||||
|
if f.data.Progress == nil {
|
||||||
if len(f.meta.Opts.SelectFiles) == 0 {
|
if len(f.meta.Opts.SelectFiles) == 0 {
|
||||||
f.meta.Opts.SelectFiles = make([]int, len(files))
|
f.meta.Opts.SelectFiles = make([]int, len(files))
|
||||||
for i := range files {
|
for i := range files {
|
||||||
f.meta.Opts.SelectFiles[i] = i
|
f.meta.Opts.SelectFiles[i] = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
f.data.Progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
|
}
|
||||||
if len(f.meta.Opts.SelectFiles) == len(files) {
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||||
f.torrent.DownloadAll()
|
f.torrent.DownloadAll()
|
||||||
} else {
|
} else {
|
||||||
@@ -126,7 +141,6 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
file.Download()
|
file.Download()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +151,12 @@ func (f *Fetcher) Pause() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Close() (err error) {
|
func (f *Fetcher) Close() (err error) {
|
||||||
f.torrentDrop.Store(false)
|
|
||||||
f.safeDrop()
|
f.safeDrop()
|
||||||
|
f.torrentDropFunc()
|
||||||
|
f.uploadDoneCh <- nil
|
||||||
|
if len(client.Torrents()) == 0 {
|
||||||
|
err = closeClient()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,21 +169,47 @@ func (f *Fetcher) safeDrop() {
|
|||||||
f.torrent.Drop()
|
f.torrent.Drop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||||
|
return f.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Stats() any {
|
||||||
|
var stats torrent.TorrentStats
|
||||||
|
if f.torrent != nil {
|
||||||
|
stats = f.torrent.Stats()
|
||||||
|
} else {
|
||||||
|
stats = torrent.TorrentStats{}
|
||||||
|
}
|
||||||
|
return &bt.Stats{
|
||||||
|
TotalPeers: stats.TotalPeers,
|
||||||
|
ActivePeers: stats.ActivePeers,
|
||||||
|
ConnectedSeeders: stats.ConnectedSeeders,
|
||||||
|
SeedBytes: f.data.SeedBytes,
|
||||||
|
SeedRatio: f.seedRadio(),
|
||||||
|
SeedTime: f.data.SeedTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
|
if !f.torrentReady.Load() {
|
||||||
|
return f.data.Progress
|
||||||
|
}
|
||||||
|
for i := range f.data.Progress {
|
||||||
|
selectIndex := f.meta.Opts.SelectFiles[i]
|
||||||
|
file := f.torrent.Files()[selectIndex]
|
||||||
|
f.data.Progress[i] = file.BytesCompleted()
|
||||||
|
}
|
||||||
|
return f.data.Progress
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Wait() (err error) {
|
func (f *Fetcher) Wait() (err error) {
|
||||||
for {
|
for {
|
||||||
if f.torrentDrop.Load() {
|
select {
|
||||||
break
|
case <-f.torrentDropCtx.Done():
|
||||||
}
|
return
|
||||||
|
case <-time.After(time.Second):
|
||||||
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
||||||
done := true
|
if f.isDone() {
|
||||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
|
||||||
file := f.torrent.Files()[selectIndex]
|
|
||||||
if file.BytesCompleted() < file.Length() {
|
|
||||||
done = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if done {
|
|
||||||
// remove unselected files
|
// remove unselected files
|
||||||
for i, file := range f.torrent.Files() {
|
for i, file := range f.torrent.Files() {
|
||||||
selected := false
|
selected := false
|
||||||
@@ -179,38 +223,24 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 500)
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
func (f *Fetcher) isDone() bool {
|
||||||
return f.meta
|
if f.meta.Opts == nil {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
func (f *Fetcher) Stats() any {
|
|
||||||
stats := f.torrent.Stats()
|
|
||||||
baseStats := &bt.Stats{
|
|
||||||
TotalPeers: stats.TotalPeers,
|
|
||||||
ActivePeers: stats.ActivePeers,
|
|
||||||
ConnectedSeeders: stats.ConnectedSeeders,
|
|
||||||
}
|
|
||||||
return baseStats
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
|
||||||
if !f.torrentReady.Load() {
|
|
||||||
return f.progress
|
|
||||||
}
|
|
||||||
for i := range f.progress {
|
|
||||||
selectIndex := f.meta.Opts.SelectFiles[i]
|
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
f.progress[i] = file.BytesCompleted()
|
if file.BytesCompleted() < file.Length() {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return f.progress
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) updateRes() {
|
func (f *Fetcher) updateRes() {
|
||||||
@@ -235,7 +265,77 @@ func (f *Fetcher) updateRes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
func (f *Fetcher) Upload() (err error) {
|
||||||
|
return f.addTorrent(f.meta.Req, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) doUpload(fromUpload bool) {
|
||||||
|
if !f.torrentUpload.CompareAndSwap(false, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and update seed data
|
||||||
|
lastData := &fetcherData{
|
||||||
|
SeedBytes: f.data.SeedBytes,
|
||||||
|
SeedTime: f.data.SeedTime,
|
||||||
|
}
|
||||||
|
var doneTime int64 = 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.torrentDropCtx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
if !f.torrentReady.Load() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := f.torrent.Stats()
|
||||||
|
f.data.SeedBytes = lastData.SeedBytes + stats.BytesWrittenData.Int64()
|
||||||
|
|
||||||
|
// Check is download complete, if not don't check and stop seeding
|
||||||
|
if !fromUpload && !f.isDone() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if doneTime == 0 {
|
||||||
|
doneTime = time.Now().Unix()
|
||||||
|
}
|
||||||
|
f.data.SeedTime = lastData.SeedTime + time.Now().Unix() - doneTime
|
||||||
|
|
||||||
|
// If the seed forever is true, keep seeding
|
||||||
|
if f.config.SeedKeep {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the seed ratio is reached, stop seeding
|
||||||
|
if f.config.SeedRatio > 0 {
|
||||||
|
seedRadio := f.seedRadio()
|
||||||
|
if seedRadio >= f.config.SeedRatio {
|
||||||
|
f.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the seed time is reached, stop seeding
|
||||||
|
if f.config.SeedTime > 0 {
|
||||||
|
if f.data.SeedTime >= f.config.SeedTime {
|
||||||
|
f.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) UploadedBytes() int64 {
|
||||||
|
return f.data.SeedBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) WaitUpload() (err error) {
|
||||||
|
<-f.uploadDoneCh
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||||
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -245,7 +345,7 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
|||||||
schema := util.ParseSchema(req.URL)
|
schema := util.ParseSchema(req.URL)
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(req.URL)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else if schema == "APPLICATION/X-BITTORRENT" {
|
} else if schema == "DATA" {
|
||||||
_, data := util.ParseDataUri(req.URL)
|
_, data := util.ParseDataUri(req.URL)
|
||||||
buf := bytes.NewBuffer(data)
|
buf := bytes.NewBuffer(data)
|
||||||
var metaInfo *metainfo.MetaInfo
|
var metaInfo *metainfo.MetaInfo
|
||||||
@@ -285,34 +385,107 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
|||||||
}
|
}
|
||||||
<-f.torrent.GotInfo()
|
<-f.torrent.GotInfo()
|
||||||
f.torrentReady.Store(true)
|
f.torrentReady.Store(true)
|
||||||
|
|
||||||
|
go f.doUpload(fromUpload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherBuilder struct {
|
func (f *Fetcher) seedRadio() float64 {
|
||||||
|
var bytesRead int64
|
||||||
|
if f.Meta().Res != nil {
|
||||||
|
bytesRead = f.Meta().Res.Size
|
||||||
|
} else {
|
||||||
|
bytesRead = 0
|
||||||
|
}
|
||||||
|
if bytesRead <= 0 {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
|
return float64(f.data.SeedBytes) / float64(bytesRead)
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Schemes() []string {
|
|
||||||
return schemes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
type fetcherData struct {
|
||||||
|
Progress fetcher.Progress
|
||||||
|
SeedBytes int64
|
||||||
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
|
SeedTime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeClient() error {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
if closeFunc != nil {
|
||||||
|
closeFunc()
|
||||||
|
}
|
||||||
|
if client != nil {
|
||||||
|
errs := client.Close()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errs[0]
|
||||||
|
}
|
||||||
|
client = nil
|
||||||
|
closeCtx = nil
|
||||||
|
closeFunc = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetcherManager struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Name() string {
|
||||||
|
return "bt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||||
|
return []*fetcher.SchemeFilter{
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeUrl,
|
||||||
|
Pattern: "MAGNET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeFile,
|
||||||
|
Pattern: "TORRENT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: fetcher.FilterTypeBase64,
|
||||||
|
Pattern: "APPLICATION/X-BITTORRENT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
return nil, nil
|
return &config{
|
||||||
|
ListenPort: 0,
|
||||||
|
Trackers: []string{},
|
||||||
|
SeedKeep: false,
|
||||||
|
SeedRatio: 1.0,
|
||||||
|
SeedTime: 120 * 60,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
_f := f.(*Fetcher)
|
||||||
|
return _f.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
|
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
return &Fetcher{
|
return &Fetcher{
|
||||||
meta: meta,
|
meta: meta,
|
||||||
|
data: v.(*fetcherData),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Close() error {
|
||||||
|
return closeClient()
|
||||||
|
}
|
||||||
|
|
||||||
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
||||||
func parseBep20() string {
|
func parseBep20() string {
|
||||||
s := strings.ReplaceAll(base.Version, ".", "")
|
s := strings.ReplaceAll(base.Version, ".", "")
|
||||||
|
|||||||
@@ -100,24 +100,26 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.ProxyConfig = proxyConfig
|
newController.ProxyConfig = proxyConfig
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +377,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 +390,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 +398,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 {
|
||||||
@@ -466,30 +457,48 @@ 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) 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 +509,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -306,7 +306,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 {
|
||||||
@@ -359,19 +359,21 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildFetcher() *Fetcher {
|
func buildFetcher() *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.(*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"
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) Clone() *Options {
|
func (o *Options) Clone() *Options {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return &Options{
|
return &Options{
|
||||||
Name: o.Name,
|
Name: o.Name,
|
||||||
Path: o.Path,
|
Path: o.Path,
|
||||||
@@ -147,12 +150,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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+156
-63
@@ -42,12 +42,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 +59,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 +82,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 +92,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 +122,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 +167,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 +191,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,19 +237,21 @@ 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)
|
||||||
}
|
}
|
||||||
ctl.ProxyConfig = d.cfg.Proxy
|
ctl.ProxyConfig = d.cfg.Proxy
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
@@ -304,7 +332,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
|
||||||
}
|
}
|
||||||
@@ -464,8 +492,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 +517,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 +533,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 +589,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,
|
||||||
|
d.storage.Close,
|
||||||
}
|
}
|
||||||
return d.storage.Close()
|
for _, fm := range d.cfg.FetchManagers {
|
||||||
|
closeArr = append(closeArr, fm.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,6 +695,38 @@ 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)
|
||||||
@@ -695,15 +781,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 +812,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 +833,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 +843,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 +899,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
|
||||||
@@ -867,10 +959,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
|
|
||||||
task.Progress.Speed = 0
|
task.Progress.Speed = 0
|
||||||
task.timer.Start()
|
task.timer.Start()
|
||||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
if err := task.fetcher.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := task.fetcher.Start(); err != nil {
|
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.emit(EventKeyStart, task)
|
d.emit(EventKeyStart, task)
|
||||||
@@ -940,12 +1032,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 +1047,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,10 +133,28 @@ 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) {
|
||||||
@@ -342,8 +380,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{
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-13
@@ -19,19 +19,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 {
|
||||||
@@ -55,29 +57,31 @@ func (t *Task) updateStatus(status base.Status) {
|
|||||||
func (t *Task) clone() *Task {
|
func (t *Task) clone() *Task {
|
||||||
return &Task{
|
return &Task{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
|
Protocol: t.Protocol,
|
||||||
Meta: t.Meta,
|
Meta: t.Meta,
|
||||||
Status: t.Status,
|
Status: t.Status,
|
||||||
|
Uploading: t.Uploading,
|
||||||
Progress: t.Progress,
|
Progress: t.Progress,
|
||||||
CreatedAt: t.CreatedAt,
|
CreatedAt: t.CreatedAt,
|
||||||
UpdatedAt: t.UpdatedAt,
|
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 +96,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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -154,7 +154,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
defer func() {
|
defer func() {
|
||||||
if v := recover(); v != nil {
|
if v := recover(); v != nil {
|
||||||
err := errors.WithStack(fmt.Errorf("%v", v))
|
err := errors.WithStack(fmt.Errorf("%v", v))
|
||||||
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
|
Downloader.Logger.Error().Stack().Err(err).Msgf("http server panic: %s %s", r.Method, r.RequestURI)
|
||||||
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
|
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
+2
-10
@@ -140,7 +140,7 @@ func TestCreateDirectTask(t *testing.T) {
|
|||||||
func TestCreateDirectTaskBatch(t *testing.T) {
|
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.Request, 0)
|
||||||
for i := 0; i < 6; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
reqs = append(reqs, createReq.Req)
|
reqs = append(reqs, createReq.Req)
|
||||||
}
|
}
|
||||||
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
||||||
@@ -583,15 +583,7 @@ func doTest(handler func()) {
|
|||||||
}()
|
}()
|
||||||
defer func() {
|
defer func() {
|
||||||
Downloader.PauseAll()
|
Downloader.PauseAll()
|
||||||
|
Downloader.DeleteByStatues(nil, true)
|
||||||
tasks := Downloader.GetTasks()
|
|
||||||
taskIds := make([]string, len(tasks))
|
|
||||||
for i, task := range tasks {
|
|
||||||
taskIds[i] = task.ID
|
|
||||||
}
|
|
||||||
for _, id := range taskIds {
|
|
||||||
Downloader.Delete(id, true)
|
|
||||||
}
|
|
||||||
os.RemoveAll(cfg.StorageDir)
|
os.RemoveAll(cfg.StorageDir)
|
||||||
}()
|
}()
|
||||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||||
|
|||||||
+1
-7
@@ -8,18 +8,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FileSchema = "FILE"
|
|
||||||
|
|
||||||
func ParseSchema(url string) string {
|
func ParseSchema(url string) string {
|
||||||
index := strings.Index(url, ":")
|
index := strings.Index(url, ":")
|
||||||
// if no schema or windows path like C:\a.txt, return FILE
|
// if no schema or windows path like C:\a.txt, return FILE
|
||||||
if index == -1 || index == 1 {
|
if index == -1 || index == 1 {
|
||||||
return FileSchema
|
return ""
|
||||||
}
|
}
|
||||||
schema := url[:index]
|
schema := url[:index]
|
||||||
if schema == "data" {
|
|
||||||
schema, _ = ParseDataUri(url)
|
|
||||||
}
|
|
||||||
return strings.ToUpper(schema)
|
return strings.ToUpper(schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +27,6 @@ func ParseDataUri(uri string) (string, []byte) {
|
|||||||
}
|
}
|
||||||
mime := matches[1]
|
mime := matches[1]
|
||||||
base64Data := matches[2]
|
base64Data := matches[2]
|
||||||
// 解码Base64数据
|
|
||||||
data, err := base64.StdEncoding.DecodeString(base64Data)
|
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
|
|||||||
Loaded 50 of 77 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user