mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4736a91904 | ||
|
|
0a4d74a513 | ||
|
|
a06549a11a | ||
|
|
63296f6950 | ||
|
|
4b4c03fd90 | ||
|
|
91dfd78b77 | ||
|
|
467ada5500 |
No files matched your search
+27
-29
@@ -4,17 +4,17 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Build platform"
|
||||
description: 'Build platform'
|
||||
required: true
|
||||
default: "all"
|
||||
default: 'all'
|
||||
test:
|
||||
description: "Test mode"
|
||||
description: 'Test mode'
|
||||
required: true
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.23"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
build-windows:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
||||
runs-on: windows-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -97,13 +97,11 @@ jobs:
|
||||
PrivilegesRequired=lowest
|
||||
OutputBaseFilename=gopeed
|
||||
SetupIconFile=..\..\assets\icon\icon.ico
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
LanguageDetectionMethod=uilanguage
|
||||
ShowLanguageDialog=yes
|
||||
CloseApplications=force
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
@@ -113,7 +111,7 @@ jobs:
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs;
|
||||
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[UninstallDelete]
|
||||
@@ -122,7 +120,7 @@ jobs:
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
"@ > setup.iss
|
||||
@@ -139,7 +137,7 @@ jobs:
|
||||
build-macos-arm64-lib:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -157,7 +155,7 @@ jobs:
|
||||
build-macos:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-13
|
||||
needs: [get-release, build-macos-arm64-lib]
|
||||
needs: [ get-release, build-macos-arm64-lib ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -216,7 +214,7 @@ jobs:
|
||||
build-linux:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -254,7 +252,7 @@ jobs:
|
||||
build-snap:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -270,31 +268,31 @@ jobs:
|
||||
run: |
|
||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
|
||||
|
||||
sudo snap install snapcraft --classic
|
||||
mkdir -p snap/gui
|
||||
cp assets/icon/icon.svg snap/gui/gopeed.svg
|
||||
|
||||
|
||||
cat>snap/snapcraft.yaml<<EOF
|
||||
name: gopeed
|
||||
version: $VERSION
|
||||
summary: A modern download manager
|
||||
description: High speed downloader that supports all platforms.
|
||||
|
||||
|
||||
confinement: strict
|
||||
base: core22
|
||||
grade: stable
|
||||
|
||||
|
||||
architectures:
|
||||
- build-on: amd64
|
||||
build-for: amd64
|
||||
|
||||
|
||||
slots:
|
||||
dbus-gopeed:
|
||||
interface: dbus
|
||||
bus: session
|
||||
name: com.gopeed.gopeed
|
||||
|
||||
|
||||
apps:
|
||||
gopeed:
|
||||
command: bin/gopeed
|
||||
@@ -347,10 +345,10 @@ jobs:
|
||||
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
||||
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
||||
EOF
|
||||
|
||||
|
||||
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
||||
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
|
||||
|
||||
|
||||
snapcraft --use-lxd
|
||||
|
||||
# Snapcraft login
|
||||
@@ -359,7 +357,7 @@ jobs:
|
||||
build-android:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -368,7 +366,7 @@ jobs:
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "17"
|
||||
java-version: "11"
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
@@ -397,7 +395,7 @@ jobs:
|
||||
build-ios:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||
runs-on: macos-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -430,7 +428,7 @@ jobs:
|
||||
build-web:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -445,11 +443,11 @@ jobs:
|
||||
run: |
|
||||
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn
|
||||
flutter build web --web-renderer html
|
||||
cd ../../
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
mkdir -p dist/zip
|
||||
|
||||
|
||||
goos_arr=(windows darwin linux)
|
||||
goarch_arr=(386 amd64 arm64)
|
||||
export CGO_ENABLED=0
|
||||
@@ -513,7 +511,7 @@ jobs:
|
||||
build-docker:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get-release]
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- name: Remove unnecessary files
|
||||
run: |
|
||||
@@ -535,7 +533,7 @@ jobs:
|
||||
- name: Build flutter web
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn
|
||||
flutter build web --web-renderer html
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
|
||||
@@ -28,7 +28,7 @@ on:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.23"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
|
||||
jobs:
|
||||
# lint:
|
||||
@@ -73,17 +73,17 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- if: runner.os == 'windows-latest'
|
||||
- if: runner.os == 'windows'
|
||||
run: |
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build windows
|
||||
- if: runner.os == 'macos-latest'
|
||||
- if: runner.os == 'macOS'
|
||||
run: |
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build macos
|
||||
- if: runner.os == 'ubuntu-latest'
|
||||
- if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn
|
||||
flutter build web --web-renderer html
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn
|
||||
flutter build web --web-renderer html
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
|
||||
@@ -71,11 +71,6 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
||||
<td><code>IPA</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Qnap</td>
|
||||
<td><code>QPKG</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web</td>
|
||||
<td></td>
|
||||
@@ -120,7 +115,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
||||
### Environment
|
||||
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### Clone
|
||||
|
||||
@@ -179,7 +174,7 @@ command:
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+2
-7
@@ -71,11 +71,6 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
||||
<td><code>IPA</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Qnap</td>
|
||||
<td><code>QPKG</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web</td>
|
||||
<td></td>
|
||||
@@ -108,7 +103,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
||||
### 環境
|
||||
|
||||
1. Go 言語 1.23+
|
||||
2. Flutter 3.24+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### クローン
|
||||
|
||||
@@ -167,7 +162,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+2
-7
@@ -71,11 +71,6 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
||||
<td><code>IPA</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Qnap</td>
|
||||
<td><code>QPKG</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web</td>
|
||||
<td></td>
|
||||
@@ -116,7 +111,7 @@ Dự án này được chia thành hai phần, phần giao diện sử dụng `f
|
||||
### Environment
|
||||
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### Clone
|
||||
|
||||
@@ -175,7 +170,7 @@ command:
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+2
-7
@@ -71,11 +71,6 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
||||
<td><code>IPA</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Qnap</td>
|
||||
<td><code>QPKG</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">前往</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web</td>
|
||||
<td></td>
|
||||
@@ -120,7 +115,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
|
||||
### 环境要求
|
||||
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### 克隆项目
|
||||
|
||||
@@ -179,7 +174,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
+3
-8
@@ -71,11 +71,6 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
||||
<td><code>IPA</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Qnap</td>
|
||||
<td><code>QPKG</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">前往</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web</td>
|
||||
<td></td>
|
||||
@@ -91,7 +86,7 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
||||
|
||||
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||
|
||||
### 使用 CLI 安裝
|
||||
### 使用CLI安裝
|
||||
|
||||
使用`go install`安裝:
|
||||
|
||||
@@ -116,7 +111,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
### 開發環境
|
||||
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### 克隆項目
|
||||
|
||||
@@ -175,7 +170,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package bind
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/GopeedLab/gopeed/pkg/api"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
)
|
||||
|
||||
// Global singleton instance
|
||||
var instance *api.Instance
|
||||
|
||||
func Init(config *model.StartConfig) (err error) {
|
||||
if instance != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
config.ProductionMode = true
|
||||
instance, err = api.Create(config)
|
||||
return
|
||||
}
|
||||
|
||||
func Invoke(request *api.Request) string {
|
||||
if instance == nil {
|
||||
return BuildResult(errors.New("instance not initialized"))
|
||||
}
|
||||
return jsonEncode(api.Invoke(instance, request))
|
||||
}
|
||||
|
||||
func BuildResult(data any) string {
|
||||
if err, ok := data.(error); ok {
|
||||
buf, _ := json.Marshal(model.NewErrorResult[any](err.Error()))
|
||||
return string(buf)
|
||||
}
|
||||
return jsonEncode(model.NewOkResult(data))
|
||||
}
|
||||
|
||||
func jsonEncode(data any) string {
|
||||
buf, _ := json.Marshal(data)
|
||||
return string(buf)
|
||||
}
|
||||
+18
-13
@@ -3,27 +3,32 @@ package main
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/GopeedLab/gopeed/bind"
|
||||
"github.com/GopeedLab/gopeed/pkg/api"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
//export Start
|
||||
func Start(cfg *C.char) (int, *C.char) {
|
||||
//export Init
|
||||
func Init(cfg *C.char) *C.char {
|
||||
var config model.StartConfig
|
||||
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
||||
return 0, C.CString(err.Error())
|
||||
return C.CString(err.Error())
|
||||
}
|
||||
config.ProductionMode = true
|
||||
realPort, err := rest.Start(&config)
|
||||
if err != nil {
|
||||
return 0, C.CString(err.Error())
|
||||
if err := bind.Init(&config); err != nil {
|
||||
return C.CString(err.Error())
|
||||
}
|
||||
return realPort, nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//export Stop
|
||||
func Stop() {
|
||||
rest.Stop()
|
||||
//export Invoke
|
||||
func Invoke(req *C.char) *C.char {
|
||||
var request api.Request
|
||||
if err := json.Unmarshal([]byte(C.GoString(req)), &request); err != nil {
|
||||
return C.CString(bind.BuildResult(err))
|
||||
}
|
||||
|
||||
return C.CString(bind.Invoke(&request))
|
||||
}
|
||||
+7
-12
@@ -3,20 +3,15 @@ package libgopeed
|
||||
// #cgo LDFLAGS: -static-libstdc++
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/GopeedLab/gopeed/bind"
|
||||
"github.com/GopeedLab/gopeed/pkg/api"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
)
|
||||
|
||||
func Start(cfg string) (int, error) {
|
||||
var config model.StartConfig
|
||||
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
config.ProductionMode = true
|
||||
return rest.Start(&config)
|
||||
func Init(cfg *model.StartConfig) error {
|
||||
return bind.Init(cfg)
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
rest.Stop()
|
||||
func Invoke(request *api.Request) string {
|
||||
return bind.Invoke(request)
|
||||
}
|
||||
+10
-4
@@ -2,15 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
// only for local development
|
||||
func main() {
|
||||
cfg := &model.StartConfig{
|
||||
Network: "tcp",
|
||||
Address: "127.0.0.1:9999",
|
||||
Storage: model.StorageBolt,
|
||||
Storage: model.StorageBolt,
|
||||
DownloadConfig: &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
Host: "127.0.0.1",
|
||||
Port: 9999,
|
||||
},
|
||||
},
|
||||
WebEnable: true,
|
||||
}
|
||||
cmd.Start(cfg)
|
||||
|
||||
+18
-14
@@ -3,10 +3,9 @@ package cmd
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/api"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -18,20 +17,22 @@ var banner string
|
||||
|
||||
func Start(cfg *model.StartConfig) {
|
||||
fmt.Println(banner)
|
||||
srv, listener, err := rest.BuildServer(cfg)
|
||||
instance, err := api.Create(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
downloadCfg, err := rest.Downloader.GetConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
downloadCfgResult := instance.GetConfig()
|
||||
if downloadCfgResult.HasError() {
|
||||
panic(downloadCfgResult.Msg)
|
||||
}
|
||||
downloadCfg := downloadCfgResult.Data
|
||||
if downloadCfg.FirstLoad {
|
||||
// Set default download config
|
||||
if cfg.DownloadConfig != nil {
|
||||
cfg.DownloadConfig.Merge(downloadCfg)
|
||||
// TODO Use PatchConfig
|
||||
rest.Downloader.PutConfig(cfg.DownloadConfig)
|
||||
instance.PutConfig(cfg.DownloadConfig)
|
||||
downloadCfg = cfg.DownloadConfig
|
||||
}
|
||||
|
||||
@@ -48,25 +49,28 @@ func Start(cfg *model.StartConfig) {
|
||||
}
|
||||
if downloadDir != "" {
|
||||
downloadCfg.DownloadDir = downloadDir
|
||||
rest.Downloader.PutConfig(downloadCfg)
|
||||
instance.PutConfig(downloadCfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
watchExit()
|
||||
|
||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
watchExit(instance)
|
||||
|
||||
port, start, err := api.ListenHttp(cfg.DownloadConfig.Http, instance)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Server start success on http://%s:%d\n", cfg.DownloadConfig.Http.Host, port)
|
||||
start()
|
||||
}
|
||||
|
||||
func watchExit() {
|
||||
func watchExit(instance *api.Instance) {
|
||||
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()
|
||||
instance.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
+4
-4
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type args struct {
|
||||
Address *string `json:"address"`
|
||||
Host *string `json:"host"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
@@ -24,7 +24,7 @@ type args struct {
|
||||
|
||||
func parse() *args {
|
||||
var cliArgs args
|
||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
cliArgs.Host = flag.String("H", "0.0.0.0", "Bind Host")
|
||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
||||
@@ -35,8 +35,8 @@ func parse() *args {
|
||||
|
||||
// args priority: config file > cli args
|
||||
cfgArgs := loadConfig(*cliArgs.configPath)
|
||||
if cfgArgs.Address == nil {
|
||||
cfgArgs.Address = cliArgs.Address
|
||||
if cfgArgs.Host == nil {
|
||||
cfgArgs.Host = cliArgs.Host
|
||||
}
|
||||
if cfgArgs.Port == nil {
|
||||
cfgArgs.Port = cliArgs.Port
|
||||
|
||||
+12
-5
@@ -5,9 +5,9 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -42,12 +42,19 @@ func main() {
|
||||
dir = filepath.Dir(exe)
|
||||
}
|
||||
|
||||
if args.DownloadConfig == nil {
|
||||
args.DownloadConfig = &base.DownloaderStoreConfig{}
|
||||
}
|
||||
args.DownloadConfig.Http = &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
Host: *args.Host,
|
||||
Port: *args.Port,
|
||||
ApiToken: *args.ApiToken,
|
||||
}
|
||||
|
||||
cfg := &model.StartConfig{
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: filepath.Join(dir, "storage"),
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
|
||||
@@ -11,9 +11,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -334,7 +332,7 @@ func (f *Fetcher) doUpload(fromUpload bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get torrent stats maybe panic, see https://github.com/anacrolix/torrent/issues/972
|
||||
// get torrent stats maybe panic, see https://github.com/anacrolix/torrent/issues/972
|
||||
func (f *Fetcher) torrentStats() torrent.TorrentStats {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -361,34 +359,17 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||
schema := util.ParseSchema(req.URL)
|
||||
if schema == "MAGNET" {
|
||||
f.torrent, err = client.AddMagnet(req.URL)
|
||||
} else {
|
||||
var reader io.Reader
|
||||
if schema == "FILE" {
|
||||
fileUrl, _ := url.Parse(req.URL)
|
||||
filePath := fileUrl.Path[1:]
|
||||
reader, err = os.Open(filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if schema == "DATA" {
|
||||
_, data := util.ParseDataUri(req.URL)
|
||||
reader = bytes.NewBuffer(data)
|
||||
} else {
|
||||
reader, err = os.Open(req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer reader.(io.Closer).Close()
|
||||
}
|
||||
|
||||
} else if schema == "DATA" {
|
||||
_, data := util.ParseDataUri(req.URL)
|
||||
buf := bytes.NewBuffer(data)
|
||||
var metaInfo *metainfo.MetaInfo
|
||||
metaInfo, err = metainfo.Load(reader)
|
||||
// Hotfix for https://github.com/anacrolix/torrent/issues/992, ignore "expected EOF" error
|
||||
// TODO remove this after the issue is fixed
|
||||
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
||||
metaInfo, err = metainfo.Load(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.torrent, err = client.AddTorrent(metaInfo)
|
||||
} else {
|
||||
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -97,7 +96,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||
}
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve Single File Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -137,29 +136,10 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
||||
}
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve Multi Files Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/test.unclean.torrent",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Resolve file scheme Torrent", func(t *testing.T) {
|
||||
file, _ := filepath.Abs("./testdata/test.unclean.torrent")
|
||||
uri := "file:///" + file
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: uri,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFetcherManager_ParseName(t *testing.T) {
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
@@ -26,9 +25,6 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const connectTimeout = 15 * time.Second
|
||||
const readTimeout = 15 * time.Second
|
||||
|
||||
type RequestError struct {
|
||||
Code int
|
||||
Msg string
|
||||
@@ -107,9 +103,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
||||
// response 206 status code, support breakpoint continuation
|
||||
res.Range = true
|
||||
// parse content length from Content-Range header, eg: bytes 0-1000/1001 or bytes 0-0/*
|
||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||
if contentTotal != "" && contentTotal != "*" {
|
||||
if contentTotal != "" {
|
||||
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -254,42 +250,23 @@ func (f *Fetcher) Wait() (err error) {
|
||||
return <-f.doneCh
|
||||
}
|
||||
|
||||
type fetchResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *Fetcher) fetch() {
|
||||
var ctx context.Context
|
||||
ctx, f.cancel = context.WithCancel(context.Background())
|
||||
f.eg, _ = errgroup.WithContext(ctx)
|
||||
chunkErrs := make([]error, len(f.chunks))
|
||||
for i := 0; i < len(f.chunks); i++ {
|
||||
i := i
|
||||
f.eg.Go(func() error {
|
||||
err := f.fetchChunk(i, ctx)
|
||||
// if canceled, fail fast
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
chunkErrs[i] = err
|
||||
return nil
|
||||
return f.fetchChunk(i, ctx)
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := f.eg.Wait()
|
||||
// error returned only if canceled, just return
|
||||
if err != nil {
|
||||
// check if canceled
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
// check all fetch results, if any error, return
|
||||
for _, chunkErr := range chunkErrs {
|
||||
if chunkErr != nil {
|
||||
err = chunkErr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
f.file.Close()
|
||||
// Update file last modified time
|
||||
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
||||
@@ -312,11 +289,24 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
for {
|
||||
// if chunk is completed, return
|
||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
if chunk.retryTimes >= maxRetries {
|
||||
return
|
||||
if !f.meta.Res.Range {
|
||||
return
|
||||
}
|
||||
// check if all failed
|
||||
allFailed := true
|
||||
for _, c := range f.chunks {
|
||||
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
||||
allFailed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allFailed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -343,9 +333,10 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||
return err
|
||||
}
|
||||
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||
// Http request success, reset retry times
|
||||
chunk.retryTimes = 0
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||
if err != nil {
|
||||
@@ -360,6 +351,7 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
// If canceled, do not retry
|
||||
@@ -427,10 +419,6 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
return
|
||||
}
|
||||
httpReq.Header = headers
|
||||
// Override Host header
|
||||
if host := headers.Get(base.HttpHeaderHost); host != "" {
|
||||
httpReq.Host = host
|
||||
}
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
@@ -464,9 +452,6 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
|
||||
func (f *Fetcher) buildClient() *http.Client {
|
||||
transport := &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: connectTimeout,
|
||||
}).DialContext,
|
||||
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
||||
@@ -514,7 +499,7 @@ func (fm *FetcherManager) ParseName(u string) string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Get filePath by URL
|
||||
// get filePath by URL
|
||||
name = path.Base(url.Path)
|
||||
// If file name is empty, use host name
|
||||
if name == "" || name == "/" || name == "." {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -60,21 +59,6 @@ func TestFetcher_Resolve(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetcher_ResolveWithHostHeader(t *testing.T) {
|
||||
fetcher := buildFetcher()
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "https://bing.com",
|
||||
Extra: &http.ReqExtra{
|
||||
Header: map[string]string{
|
||||
"Host": "test",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "400") {
|
||||
t.Errorf("Resolve() got = %v, want %v", err, "400")
|
||||
}
|
||||
}
|
||||
|
||||
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
||||
listener := startTestServer()
|
||||
defer listener.Close()
|
||||
@@ -148,14 +132,6 @@ func TestFetcher_DownloadLimit(t *testing.T) {
|
||||
downloadNormal(listener, 8, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
|
||||
listener := test.StartTestLimitServer(16, readTimeout.Milliseconds()+5000)
|
||||
defer listener.Close()
|
||||
|
||||
downloadError(listener, 1, t)
|
||||
downloadError(listener, 4, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadResume(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
defer listener.Close()
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimeoutReader struct {
|
||||
reader io.Reader
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewTimeoutReader(r io.Reader, timeout time.Duration) *TimeoutReader {
|
||||
return &TimeoutReader{
|
||||
reader: r,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *TimeoutReader) Read(p []byte) (n int, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tr.timeout)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
var readErr error
|
||||
var bytesRead int
|
||||
|
||||
go func() {
|
||||
bytesRead, readErr = tr.reader.Read(p)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return bytesRead, readErr
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTimeoutReader_Read(t *testing.T) {
|
||||
data := []byte("Hello, World!")
|
||||
reader := bytes.NewReader(data)
|
||||
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||
|
||||
buf := make([]byte, len(data))
|
||||
n, err := timeoutReader.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if n != len(data) {
|
||||
t.Fatalf("expected to read %d bytes, read %d", len(data), n)
|
||||
}
|
||||
if !bytes.Equal(buf, data) {
|
||||
t.Fatalf("expected %s, got %s", data, buf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutReader_ReadTimeout(t *testing.T) {
|
||||
reader := &slowReader{delay: 2 * time.Second}
|
||||
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||
|
||||
buf := make([]byte, 8192)
|
||||
_, err := timeoutReader.Read(buf)
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error, got nil")
|
||||
}
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expected %v, got %v", context.DeadlineExceeded, err)
|
||||
}
|
||||
}
|
||||
|
||||
type slowReader struct {
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (sr *slowReader) Read(p []byte) (n int, err error) {
|
||||
time.Sleep(sr.delay)
|
||||
return 0, io.EOF
|
||||
}
|
||||
+19
-30
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/armon/go-socks5"
|
||||
@@ -37,7 +36,7 @@ const (
|
||||
)
|
||||
|
||||
func StartTestFileServer() net.Listener {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return startTestServer(func() http.Handler {
|
||||
return http.FileServer(http.Dir(Dir))
|
||||
})
|
||||
}
|
||||
@@ -53,7 +52,7 @@ func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return startTestServer(func() http.Handler {
|
||||
return &SlowFileServer{
|
||||
delay: delay,
|
||||
handler: http.FileServer(http.Dir(Dir)),
|
||||
@@ -62,7 +61,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||
}
|
||||
|
||||
func StartTestCustomServer() net.Listener {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return startTestServer(func() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
file, err := os.Open(BuildFile)
|
||||
@@ -89,7 +88,7 @@ func StartTestCustomServer() net.Listener {
|
||||
|
||||
func StartTestRetryServer() net.Listener {
|
||||
counter := 0
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return startTestServer(func() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
counter++
|
||||
@@ -109,7 +108,7 @@ func StartTestRetryServer() net.Listener {
|
||||
}
|
||||
|
||||
func StartTestPostServer() net.Listener {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return startTestServer(func() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
||||
@@ -133,7 +132,7 @@ func StartTestPostServer() net.Listener {
|
||||
|
||||
func StartTestErrorServer() net.Listener {
|
||||
counter := 0
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return startTestServer(func() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
counter++
|
||||
@@ -150,7 +149,7 @@ func StartTestErrorServer() net.Listener {
|
||||
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
var connections atomic.Int32
|
||||
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return startTestServer(func() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
defer func() {
|
||||
@@ -166,14 +165,12 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
if r == "" {
|
||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||
writer.WriteHeader(200)
|
||||
(writer.(http.Flusher)).Flush()
|
||||
|
||||
file, err := os.Open(BuildFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
slowCopy(sl, writer, file, delay)
|
||||
slowCopy(writer, file, delay)
|
||||
} else {
|
||||
// split range
|
||||
s := strings.Split(r, "=")
|
||||
@@ -207,8 +204,6 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||
writer.Header().Set("Accept-Ranges", "bytes")
|
||||
writer.WriteHeader(206)
|
||||
(writer.(http.Flusher)).Flush()
|
||||
|
||||
file, err := os.Open(BuildFile)
|
||||
if err != nil {
|
||||
writer.WriteHeader(500)
|
||||
@@ -216,7 +211,7 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
}
|
||||
defer file.Close()
|
||||
file.Seek(start, 0)
|
||||
slowCopyN(sl, writer, file, end-start+1, delay)
|
||||
slowCopyN(writer, file, end-start+1, delay)
|
||||
}
|
||||
})
|
||||
return mux
|
||||
@@ -224,12 +219,9 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
}
|
||||
|
||||
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
|
||||
func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
||||
func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
if sl.isShutdown {
|
||||
return 0, errors.New("server shutdown")
|
||||
}
|
||||
nr, er := src.Read(buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write(buf[0:nr])
|
||||
@@ -258,8 +250,8 @@ func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (
|
||||
return written, err
|
||||
}
|
||||
|
||||
func slowCopyN(sl *shutdownListener, dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||
written, err = slowCopy(sl, dst, io.LimitReader(src, n), delay)
|
||||
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
|
||||
if written == n {
|
||||
return n, nil
|
||||
}
|
||||
@@ -270,7 +262,7 @@ func slowCopyN(sl *shutdownListener, dst io.Writer, src io.Reader, n int64, dela
|
||||
return
|
||||
}
|
||||
|
||||
func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.Listener {
|
||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -280,7 +272,7 @@ func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.L
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
// Write random data
|
||||
// 随机生成一个文件
|
||||
l := int64(8192)
|
||||
buf := make([]byte, l)
|
||||
size := int64(0)
|
||||
@@ -297,24 +289,21 @@ func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.L
|
||||
size += l
|
||||
}
|
||||
server := &http.Server{}
|
||||
sl := &shutdownListener{
|
||||
server.Handler = serverHandle()
|
||||
go server.Serve(listener)
|
||||
|
||||
return &shutdownListener{
|
||||
server: server,
|
||||
Listener: listener,
|
||||
}
|
||||
server.Handler = serverHandle(sl)
|
||||
go server.Serve(listener)
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
type shutdownListener struct {
|
||||
server *http.Server
|
||||
isShutdown bool
|
||||
server *http.Server
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func (c *shutdownListener) Close() error {
|
||||
c.isShutdown = true
|
||||
closeErr := c.server.Shutdown(context.Background())
|
||||
if err := ifExistAndRemove(BuildFile); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
startCfg *model.StartConfig
|
||||
downloader *download.Downloader
|
||||
|
||||
httpLock sync.Mutex
|
||||
srv *http.Server
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
func Create(startCfg *model.StartConfig) (*Instance, error) {
|
||||
if startCfg == nil {
|
||||
startCfg = &model.StartConfig{}
|
||||
}
|
||||
startCfg.Init()
|
||||
|
||||
i := &Instance{
|
||||
startCfg: startCfg,
|
||||
}
|
||||
|
||||
downloadCfg := &download.DownloaderConfig{
|
||||
ProductionMode: startCfg.ProductionMode,
|
||||
RefreshInterval: startCfg.RefreshInterval,
|
||||
}
|
||||
if startCfg.Storage == model.StorageBolt {
|
||||
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
||||
} else {
|
||||
downloadCfg.Storage = download.NewMemStorage()
|
||||
}
|
||||
downloadCfg.StorageDir = startCfg.StorageDir
|
||||
downloadCfg.Init()
|
||||
downloader := download.NewDownloader(downloadCfg)
|
||||
if err := downloader.Setup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.downloader = downloader
|
||||
i.httpLock = sync.Mutex{}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *Instance) Listener(listener download.Listener) *model.Result[any] {
|
||||
i.downloader.Listener(listener)
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) StartHttp() *model.Result[int] {
|
||||
i.httpLock.Lock()
|
||||
defer i.httpLock.Unlock()
|
||||
|
||||
return i.startHttp()
|
||||
}
|
||||
|
||||
func (i *Instance) startHttp() *model.Result[int] {
|
||||
httpCfg := i.getHttpConfig()
|
||||
if httpCfg == nil || !httpCfg.Enable {
|
||||
return model.NewErrorResult[int]("HTTP API server not enabled")
|
||||
}
|
||||
if i.srv != nil {
|
||||
return model.NewErrorResult[int]("HTTP API server already started")
|
||||
}
|
||||
|
||||
port, start, err := ListenHttp(httpCfg, i)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[int](err.Error())
|
||||
}
|
||||
httpCfg.RunningPort = port
|
||||
go start()
|
||||
return model.NewOkResult(port)
|
||||
}
|
||||
|
||||
func (i *Instance) getHttpConfig() *base.DownloaderHttpConfig {
|
||||
var httpCfg *base.DownloaderHttpConfig
|
||||
if i.startCfg.DownloadConfig != nil && i.startCfg.DownloadConfig.Http != nil {
|
||||
httpCfg = i.startCfg.DownloadConfig.Http
|
||||
} else {
|
||||
cfg, _ := i.downloader.GetConfig()
|
||||
httpCfg = cfg.Http
|
||||
}
|
||||
return httpCfg
|
||||
}
|
||||
|
||||
func (i *Instance) StopHttp() *model.Result[any] {
|
||||
i.httpLock.Lock()
|
||||
defer i.httpLock.Unlock()
|
||||
|
||||
return i.stopHttp()
|
||||
}
|
||||
|
||||
func (i *Instance) stopHttp() *model.Result[any] {
|
||||
if i.srv != nil {
|
||||
if err := i.srv.Shutdown(context.Background()); err != nil {
|
||||
i.downloader.Logger.Warn().Err(err).Msg("shutdown http server failed")
|
||||
}
|
||||
i.srv = nil
|
||||
i.listener = nil
|
||||
}
|
||||
httpCfg := i.getHttpConfig()
|
||||
if httpCfg != nil {
|
||||
httpCfg.RunningPort = 0
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) RestartHttp() *model.Result[int] {
|
||||
i.httpLock.Lock()
|
||||
defer i.httpLock.Unlock()
|
||||
|
||||
i.stopHttp()
|
||||
return i.startHttp()
|
||||
}
|
||||
|
||||
func (i *Instance) Info() *model.Result[map[string]any] {
|
||||
return model.NewOkResult(map[string]any{
|
||||
"version": base.Version,
|
||||
"runtime": runtime.Version(),
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"inDocker": base.InDocker == "true",
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Instance) Resolve(req *base.Request) *model.Result[*download.ResolveResult] {
|
||||
rr, err := i.downloader.Resolve(req)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[*download.ResolveResult](err.Error())
|
||||
}
|
||||
return model.NewOkResult(rr)
|
||||
}
|
||||
|
||||
func (i *Instance) CreateTask(req *model.CreateTask) *model.Result[string] {
|
||||
var (
|
||||
taskId string
|
||||
err error
|
||||
)
|
||||
if req.Rid != "" {
|
||||
taskId, err = i.downloader.Create(req.Rid, req.Opt)
|
||||
} else if req.Req != nil {
|
||||
taskId, err = i.downloader.CreateDirect(req.Req, req.Opt)
|
||||
} else {
|
||||
return model.NewErrorResult[string]("param invalid: rid or req", model.CodeInvalidParam)
|
||||
}
|
||||
if err != nil {
|
||||
return model.NewErrorResult[string](err.Error())
|
||||
}
|
||||
return model.NewOkResult(taskId)
|
||||
}
|
||||
|
||||
func (i *Instance) CreateTaskBatch(req *model.CreateTaskBatch) *model.Result[[]string] {
|
||||
if len(req.Reqs) == 0 {
|
||||
return model.NewErrorResult[[]string]("param invalid: reqs", model.CodeInvalidParam)
|
||||
}
|
||||
taskIds, err := i.downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[[]string](err.Error())
|
||||
}
|
||||
return model.NewOkResult(taskIds)
|
||||
}
|
||||
|
||||
func (i *Instance) PauseTask(taskId string) *model.Result[any] {
|
||||
err := i.downloader.Pause(&download.TaskFilter{
|
||||
ID: []string{taskId},
|
||||
})
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) PauseTasks(filter *download.TaskFilter) *model.Result[any] {
|
||||
err := i.downloader.Pause(filter)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) ContinueTask(taskId string) *model.Result[any] {
|
||||
err := i.downloader.Continue(&download.TaskFilter{
|
||||
ID: []string{taskId},
|
||||
})
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) ContinueTasks(filter *download.TaskFilter) *model.Result[any] {
|
||||
err := i.downloader.Continue(filter)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) DeleteTask(taskId string, force bool) *model.Result[any] {
|
||||
err := i.downloader.Delete(&download.TaskFilter{
|
||||
ID: []string{taskId},
|
||||
}, force)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) DeleteTasks(filter *download.TaskFilter, force bool) *model.Result[any] {
|
||||
err := i.downloader.Delete(filter, force)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) GetTask(taskId string) *model.Result[*download.Task] {
|
||||
if taskId == "" {
|
||||
return model.NewErrorResult[*download.Task]("param invalid: id", model.CodeInvalidParam)
|
||||
}
|
||||
task := i.downloader.GetTask(taskId)
|
||||
if task == nil {
|
||||
return model.NewErrorResult[*download.Task]("task not found", model.CodeTaskNotFound)
|
||||
}
|
||||
return model.NewOkResult(task)
|
||||
}
|
||||
|
||||
func (i *Instance) GetTasks(filter *download.TaskFilter) *model.Result[[]*download.Task] {
|
||||
return model.NewOkResult(i.downloader.GetTasksByFilter(filter))
|
||||
}
|
||||
|
||||
func (i *Instance) GetTaskStats(taskId string) *model.Result[any] {
|
||||
stats, err := i.downloader.GetTaskStats(taskId)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewOkResult(stats)
|
||||
}
|
||||
|
||||
func (i *Instance) GetConfig() *model.Result[*base.DownloaderStoreConfig] {
|
||||
config, err := i.downloader.GetConfig()
|
||||
if err != nil {
|
||||
return model.NewErrorResult[*base.DownloaderStoreConfig](err.Error())
|
||||
}
|
||||
return model.NewOkResult(config)
|
||||
}
|
||||
|
||||
func (i *Instance) PutConfig(cfg *base.DownloaderStoreConfig) *model.Result[any] {
|
||||
err := i.downloader.PutConfig(cfg)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) InstallExtension(req *model.InstallExtension) *model.Result[string] {
|
||||
var (
|
||||
installedExt *download.Extension
|
||||
err error
|
||||
)
|
||||
if req.DevMode {
|
||||
installedExt, err = i.downloader.InstallExtensionByFolder(req.URL, true)
|
||||
} else {
|
||||
installedExt, err = i.downloader.InstallExtensionByGit(req.URL)
|
||||
}
|
||||
if err != nil {
|
||||
return model.NewErrorResult[string](err.Error())
|
||||
}
|
||||
|
||||
return model.NewOkResult(installedExt.Identity)
|
||||
}
|
||||
|
||||
func (i *Instance) GetExtension(identity string) *model.Result[*download.Extension] {
|
||||
extension, err := i.downloader.GetExtension(identity)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[*download.Extension](err.Error())
|
||||
}
|
||||
return model.NewOkResult(extension)
|
||||
}
|
||||
|
||||
func (i *Instance) GetExtensions() *model.Result[[]*download.Extension] {
|
||||
return model.NewOkResult(i.downloader.GetExtensions())
|
||||
}
|
||||
|
||||
func (i *Instance) UpdateExtensionSettings(identity string, req *model.UpdateExtensionSettings) *model.Result[any] {
|
||||
err := i.downloader.UpdateExtensionSettings(identity, req.Settings)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) SwitchExtension(identity string, req *model.SwitchExtension) *model.Result[any] {
|
||||
err := i.downloader.SwitchExtension(identity, req.Status)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) DeleteExtension(identity string) *model.Result[any] {
|
||||
err := i.downloader.DeleteExtension(identity)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) UpgradeCheckExtension(identity string) *model.Result[*model.UpgradeCheckExtensionResp] {
|
||||
newVersion, err := i.downloader.UpgradeCheckExtension(identity)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[*model.UpgradeCheckExtensionResp](err.Error())
|
||||
}
|
||||
|
||||
return model.NewOkResult(&model.UpgradeCheckExtensionResp{
|
||||
NewVersion: newVersion,
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Instance) UpgradeExtension(identity string) *model.Result[any] {
|
||||
err := i.downloader.UpgradeExtension(identity)
|
||||
if err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) Close() *model.Result[any] {
|
||||
i.StopHttp()
|
||||
if i.downloader != nil {
|
||||
if err := i.downloader.Close(); err != nil {
|
||||
i.downloader.Logger.Warn().Err(err).Msg("close downloader failed")
|
||||
}
|
||||
i.downloader = nil
|
||||
}
|
||||
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
func (i *Instance) Clear() *model.Result[any] {
|
||||
if err := i.downloader.Clear(); err != nil {
|
||||
return model.NewErrorResult[any](err.Error())
|
||||
}
|
||||
return model.NewNilResult()
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Method string `json:"method"`
|
||||
Params []any `json:"params"`
|
||||
}
|
||||
|
||||
// Invoke support dynamic call method
|
||||
func Invoke(instance *Instance, request *Request) (ret any) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
ret = model.NewErrorResult[any](fmt.Sprintf("%v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
method, args := request.Method, request.Params
|
||||
dsType := reflect.ValueOf(instance)
|
||||
fn := dsType.MethodByName(method)
|
||||
numIn := fn.Type().NumIn()
|
||||
in := make([]reflect.Value, numIn)
|
||||
for i := 0; i < numIn; i++ {
|
||||
paramType := fn.Type().In(i)
|
||||
arg := args[i]
|
||||
if arg == nil {
|
||||
in[i] = reflect.Zero(fn.Type().In(i))
|
||||
continue
|
||||
}
|
||||
var param reflect.Value
|
||||
var paramPtr any
|
||||
if paramType.Kind() == reflect.Ptr {
|
||||
param = reflect.New(paramType.Elem())
|
||||
paramPtr = param.Interface()
|
||||
} else {
|
||||
param = reflect.New(paramType).Elem()
|
||||
paramPtr = param.Addr().Interface()
|
||||
}
|
||||
if err := util.MapToStruct(arg, paramPtr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
in[i] = param
|
||||
}
|
||||
retVals := fn.Call(in)
|
||||
return retVals[0].Interface()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInvoke(t *testing.T) {
|
||||
doTestInvoke(t, "Info", []any{}, model.CodeOk)
|
||||
doTestInvoke(t, "Info1", []any{}, model.CodeError)
|
||||
doTestInvoke(t, "CreateTask", []any{nil}, model.CodeError)
|
||||
doTestInvoke(t, "GetTasks", nil, model.CodeError)
|
||||
doTestInvoke(t, "GetTasks", []any{}, model.CodeError)
|
||||
doTestInvoke(t, "GetTasks", []any{"{abc:123"}, model.CodeError)
|
||||
doTestInvoke(t, "GetTasks", []any{&download.TaskFilter{}}, model.CodeOk)
|
||||
doTestInvoke(t, "SwitchExtension", []any{"test", &model.SwitchExtension{Status: true}}, model.CodeError)
|
||||
}
|
||||
|
||||
func doTestInvoke(t *testing.T, method string, params []any, expectCode model.RespCode) {
|
||||
instance, _ := Create(nil)
|
||||
defer instance.Close()
|
||||
|
||||
result := Invoke(instance, &Request{
|
||||
Method: method,
|
||||
Params: params,
|
||||
})
|
||||
buf, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var res model.Result[any]
|
||||
if err := json.Unmarshal(buf, &res); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res.Code != expectCode {
|
||||
t.Fatalf("Invoke method [%s] failed, expect code [%d], got [%d], msg: %s", method, expectCode, res.Code, res.Msg)
|
||||
}
|
||||
}
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const instanceKey string = "apiInstance"
|
||||
|
||||
func ListenHttp(httpCfg *base.DownloaderHttpConfig, ai *Instance) (int, func(), error) {
|
||||
var r = mux.NewRouter()
|
||||
r.Methods(http.MethodGet).Path("/api/v1/info").HandlerFunc(Info)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseTasks)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueTasks)
|
||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}/stats").HandlerFunc(GetTaskStats)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions").HandlerFunc(GetExtensions)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}").HandlerFunc(GetExtension)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/settings").HandlerFunc(UpdateExtensionSettings)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/switch").HandlerFunc(SwitchExtension)
|
||||
r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/upgrade").HandlerFunc(UpgradeCheckExtension)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/upgrade").HandlerFunc(UpgradeExtension)
|
||||
if ai.startCfg.WebEnable {
|
||||
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(&taskFileSystem{
|
||||
downloader: ai.downloader,
|
||||
}))
|
||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(&extensionFileSystem{
|
||||
downloader: ai.downloader,
|
||||
}))
|
||||
r.PathPrefix("/").Handler(http.FileServer(http.FS(ai.startCfg.WebFS)))
|
||||
}
|
||||
|
||||
r.Use(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), instanceKey, ai)
|
||||
h.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
})
|
||||
|
||||
if httpCfg.ApiToken != "" || (ai.startCfg.WebEnable && ai.startCfg.WebBasicAuth != nil) {
|
||||
r.Use(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if httpCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == httpCfg.ApiToken {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if ai.startCfg.WebEnable && ai.startCfg.WebBasicAuth != nil {
|
||||
if r.Header.Get("Authorization") == ai.startCfg.WebBasicAuth.Authorization() {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
|
||||
}
|
||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult[any]("unauthorized", model.CodeUnauthorized))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// recover panic
|
||||
r.Use(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
err := errors.WithStack(fmt.Errorf("%v", v))
|
||||
ai.downloader.Logger.Error().Stack().Err(err).Msgf("http server panic: %s %s", r.Method, r.RequestURI)
|
||||
WriteJson(w, model.NewErrorResult[any](err.Error(), model.CodeError))
|
||||
}
|
||||
}()
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
srv := &http.Server{Handler: handlers.CORS(
|
||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||
handlers.AllowedOrigins([]string{"*"}),
|
||||
)(r)}
|
||||
|
||||
listener, err := net.Listen("tcp", net.JoinHostPort(httpCfg.Host, fmt.Sprintf("%d", httpCfg.Port)))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
ai.srv = srv
|
||||
ai.listener = listener
|
||||
|
||||
return listener.Addr().(*net.TCPAddr).Port, func() {
|
||||
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
ai.downloader.Logger.Error().Err(err).Msg("http server listen error")
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getInstance(r *http.Request) *Instance {
|
||||
return r.Context().Value(instanceKey).(*Instance)
|
||||
}
|
||||
|
||||
func Info(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJson(w, getInstance(r).Info())
|
||||
}
|
||||
|
||||
func Resolve(w http.ResponseWriter, r *http.Request) {
|
||||
var req base.Request
|
||||
if ReadJson(r, w, &req) {
|
||||
WriteJson(w, getInstance(r).Resolve(&req))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTask
|
||||
if ReadJson(r, w, &req) {
|
||||
WriteJson(w, getInstance(r).CreateTask(&req))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTaskBatch
|
||||
if ReadJson(r, w, &req) {
|
||||
WriteJson(w, getInstance(r).CreateTaskBatch(&req))
|
||||
}
|
||||
}
|
||||
|
||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||
var taskId string
|
||||
if !parseTaskId(r, w, &taskId) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).PauseTask(taskId))
|
||||
}
|
||||
|
||||
func PauseTasks(w http.ResponseWriter, r *http.Request) {
|
||||
var filter download.TaskFilter
|
||||
if !parseFilter(r, w, &filter) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).PauseTasks(&filter))
|
||||
}
|
||||
|
||||
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
||||
var taskId string
|
||||
if !parseTaskId(r, w, &taskId) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).ContinueTask(taskId))
|
||||
}
|
||||
|
||||
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
|
||||
var filter download.TaskFilter
|
||||
if !parseFilter(r, w, &filter) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).ContinueTasks(&filter))
|
||||
}
|
||||
|
||||
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||
var taskId string
|
||||
if !parseTaskId(r, w, &taskId) {
|
||||
return
|
||||
}
|
||||
force := r.FormValue("force")
|
||||
WriteJson(w, getInstance(r).DeleteTask(taskId, force == "true"))
|
||||
}
|
||||
|
||||
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
||||
var filter download.TaskFilter
|
||||
if !parseFilter(r, w, &filter) {
|
||||
return
|
||||
}
|
||||
force := r.FormValue("force")
|
||||
WriteJson(w, getInstance(r).DeleteTasks(&filter, force == "true"))
|
||||
}
|
||||
|
||||
func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||
var taskId string
|
||||
if !parseTaskId(r, w, &taskId) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).GetTask(taskId))
|
||||
}
|
||||
|
||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||
var filter download.TaskFilter
|
||||
if !parseFilter(r, w, &filter) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).GetTasks(&filter))
|
||||
}
|
||||
|
||||
func GetTaskStats(w http.ResponseWriter, r *http.Request) {
|
||||
var taskId string
|
||||
if !parseTaskId(r, w, &taskId) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).GetTaskStats(taskId))
|
||||
}
|
||||
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJson(w, getInstance(r).GetConfig())
|
||||
}
|
||||
|
||||
func PutConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var cfg base.DownloaderStoreConfig
|
||||
if !ReadJson(r, w, &cfg) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).PutConfig(&cfg))
|
||||
}
|
||||
|
||||
func InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.InstallExtension
|
||||
if !ReadJson(r, w, &req) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).InstallExtension(&req))
|
||||
}
|
||||
|
||||
func GetExtensions(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJson(w, getInstance(r).GetExtensions())
|
||||
}
|
||||
|
||||
func GetExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var identity string
|
||||
if !parseIdentity(r, w, &identity) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).GetExtension(identity))
|
||||
}
|
||||
|
||||
func UpdateExtensionSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var identity string
|
||||
if !parseIdentity(r, w, &identity) {
|
||||
return
|
||||
}
|
||||
var req model.UpdateExtensionSettings
|
||||
if !ReadJson(r, w, &req) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).UpdateExtensionSettings(identity, &req))
|
||||
}
|
||||
|
||||
func SwitchExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var identity string
|
||||
if !parseIdentity(r, w, &identity) {
|
||||
return
|
||||
}
|
||||
var switchExtension model.SwitchExtension
|
||||
if !ReadJson(r, w, &switchExtension) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).SwitchExtension(identity, &switchExtension))
|
||||
}
|
||||
|
||||
func DeleteExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var identity string
|
||||
if !parseIdentity(r, w, &identity) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).DeleteExtension(identity))
|
||||
}
|
||||
|
||||
func UpgradeCheckExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var identity string
|
||||
if !parseIdentity(r, w, &identity) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).UpgradeCheckExtension(identity))
|
||||
}
|
||||
|
||||
func UpgradeExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var identity string
|
||||
if !parseIdentity(r, w, &identity) {
|
||||
return
|
||||
}
|
||||
WriteJson(w, getInstance(r).UpgradeExtension(identity))
|
||||
}
|
||||
|
||||
func DoProxy(w http.ResponseWriter, r *http.Request) {
|
||||
target := r.Header.Get("X-Target-Uri")
|
||||
if target == "" {
|
||||
writeError(w, "param invalid: X-Target-Uri")
|
||||
return
|
||||
}
|
||||
targetUrl, err := url.Parse(target)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
r.RequestURI = ""
|
||||
r.URL = targetUrl
|
||||
r.Host = targetUrl.Host
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for k, vv := range resp.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func parsePathParam(r *http.Request, w http.ResponseWriter, name string, value *string) bool {
|
||||
vars := mux.Vars(r)
|
||||
*value = vars[name]
|
||||
if *value == "" {
|
||||
WriteJson(w, model.NewErrorResult[any]("param invalid: id", model.CodeInvalidParam))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseTaskId(r *http.Request, w http.ResponseWriter, taskId *string) bool {
|
||||
return parsePathParam(r, w, "id", taskId)
|
||||
}
|
||||
|
||||
func parseIdentity(r *http.Request, w http.ResponseWriter, identity *string) bool {
|
||||
return parsePathParam(r, w, "identity", identity)
|
||||
}
|
||||
|
||||
func parseFilter(r *http.Request, w http.ResponseWriter, filter *download.TaskFilter) bool {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
WriteJson(w, model.NewErrorResult[any](err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
filter.ID = r.Form["id"]
|
||||
filter.Status = convertStatues(r.Form["status"])
|
||||
filter.NotStatus = convertStatues(r.Form["notStatus"])
|
||||
return true
|
||||
}
|
||||
|
||||
func convertStatues(statues []string) []base.Status {
|
||||
result := make([]base.Status, 0)
|
||||
for _, status := range statues {
|
||||
result = append(result, base.Status(status))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, msg string) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
WriteJson(w, model.NewErrorResult[any](err.Error()))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func WriteJson(w http.ResponseWriter, v any) {
|
||||
WriteStatusJson(w, http.StatusOK, v)
|
||||
}
|
||||
|
||||
func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func resolvePath(urlPath string, prefix string) (identity string, path string, err error) {
|
||||
// remove prefix
|
||||
clearPath := strings.TrimPrefix(urlPath, prefix)
|
||||
// match extension identity, eg: /fs/extensions/identity/xxx
|
||||
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
|
||||
if !reg.MatchString(clearPath) {
|
||||
err = os.ErrNotExist
|
||||
return
|
||||
}
|
||||
matched := reg.FindStringSubmatch(clearPath)
|
||||
if len(matched) != 3 {
|
||||
err = os.ErrNotExist
|
||||
return
|
||||
}
|
||||
return matched[1], matched[2], nil
|
||||
}
|
||||
|
||||
// handle task file resource
|
||||
type taskFileSystem struct {
|
||||
downloader *download.Downloader
|
||||
}
|
||||
|
||||
func (e *taskFileSystem) Open(name string) (http.File, error) {
|
||||
// get extension identity
|
||||
identity, path, err := resolvePath(name, "/fs/tasks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task := e.downloader.GetTask(identity)
|
||||
if task == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return os.Open(filepath.Join(task.Meta.RootDirPath(), path))
|
||||
}
|
||||
|
||||
// handle extension file resource
|
||||
type extensionFileSystem struct {
|
||||
downloader *download.Downloader
|
||||
}
|
||||
|
||||
func (e *extensionFileSystem) Open(name string) (http.File, error) {
|
||||
// get extension identity
|
||||
identity, path, err := resolvePath(name, "/fs/extensions")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extension, err := e.downloader.GetExtension(identity)
|
||||
if err != nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
extensionPath := e.downloader.ExtensionPath(extension)
|
||||
return os.Open(filepath.Join(extensionPath, path))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package rest
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -21,7 +21,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
restPort int
|
||||
httpInstance *Instance
|
||||
restPort int
|
||||
|
||||
taskReq = &base.Request{
|
||||
Extra: map[string]any{
|
||||
@@ -90,7 +91,7 @@ func TestCreateTask(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -117,7 +118,7 @@ func TestCreateDirectTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -156,7 +157,7 @@ func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -180,7 +181,7 @@ func TestPauseAndContinueTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
switch event.Key {
|
||||
case download.EventKeyFinally:
|
||||
wg.Done()
|
||||
@@ -214,10 +215,7 @@ func TestPauseAndContinueTask(t *testing.T) {
|
||||
|
||||
func TestPauseAllAndContinueALLTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
cfg, err := Downloader.GetConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := UnwrapResult(httpInstance.GetConfig())
|
||||
|
||||
createAndPause := func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
@@ -275,7 +273,7 @@ func TestDeleteAllTasks(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -301,7 +299,7 @@ func TestDeleteTasksByStatues(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -325,7 +323,7 @@ func TestGetTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -354,7 +352,10 @@ func TestGetAndPutConfig(t *testing.T) {
|
||||
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
cfg.DownloadDir = "./download"
|
||||
cfg.Extra = map[string]any{
|
||||
"serverConfig": &Config{
|
||||
"serverConfig": &struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
Host: "127.0.0.1",
|
||||
Port: 8080,
|
||||
},
|
||||
@@ -446,16 +447,16 @@ func TestDeleteExtension(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateCheckExtension(t *testing.T) {
|
||||
func TestUpgradeCheckExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
resp := httpRequestCheckOk[*model.UpdateCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
resp := httpRequestCheckOk[*model.UpgradeCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/upgrade", nil)
|
||||
// no new version
|
||||
if resp.NewVersion != "" {
|
||||
t.Errorf("UpdateCheckExtension() got = %v, want %v", resp.NewVersion, "")
|
||||
t.Errorf("UpgradeCheckExtension() got = %v, want %v", resp.NewVersion, "")
|
||||
}
|
||||
// force update
|
||||
httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/upgrade", nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -506,13 +507,19 @@ func TestDoProxy(t *testing.T) {
|
||||
func TestApiToken(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.ApiToken = "123456"
|
||||
fileListener := doStart(cfg)
|
||||
cfg.DownloadConfig = &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
ApiToken: "123456",
|
||||
},
|
||||
}
|
||||
httpInstance, _ = Create(cfg)
|
||||
fileListener := doStart()
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
httpInstance.Close()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
@@ -521,7 +528,7 @@ func TestApiToken(t *testing.T) {
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
"X-Api-Token": cfg.DownloadConfig.Http.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusOK)
|
||||
@@ -532,18 +539,24 @@ func TestApiToken(t *testing.T) {
|
||||
func TestAuthorization(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.ApiToken = "123456"
|
||||
cfg.DownloadConfig = &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
ApiToken: "123456",
|
||||
},
|
||||
}
|
||||
cfg.WebEnable = true
|
||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
||||
Username: "admin",
|
||||
Password: "123456",
|
||||
}
|
||||
fileListener := doStart(cfg)
|
||||
httpInstance, _ = Create(cfg)
|
||||
fileListener := doStart()
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
httpInstance.Close()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
@@ -559,7 +572,7 @@ func TestAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
"X-Api-Token": cfg.DownloadConfig.Http.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||
@@ -573,17 +586,23 @@ func doTest(handler func()) {
|
||||
cfg.Storage = storage
|
||||
cfg.StorageDir = ".test_storage"
|
||||
cfg.WebEnable = true
|
||||
fileListener := doStart(cfg)
|
||||
cfg.DownloadConfig = &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
},
|
||||
}
|
||||
httpInstance, _ = Create(cfg)
|
||||
fileListener := doStart()
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Stop()
|
||||
Downloader.Clear()
|
||||
httpInstance.StopHttp()
|
||||
httpInstance.Clear()
|
||||
}()
|
||||
defer func() {
|
||||
Downloader.Pause(nil)
|
||||
Downloader.Delete(nil, true)
|
||||
httpInstance.PauseTasks(nil)
|
||||
httpInstance.DeleteTasks(nil, true)
|
||||
os.RemoveAll(cfg.StorageDir)
|
||||
}()
|
||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||
@@ -593,15 +612,19 @@ func doTest(handler func()) {
|
||||
testFunc(model.StorageBolt)
|
||||
}
|
||||
|
||||
func doStart(cfg *model.StartConfig) net.Listener {
|
||||
port, err := Start(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
func doStart() net.Listener {
|
||||
port := UnwrapResult(httpInstance.StartHttp())
|
||||
restPort = port
|
||||
return test.StartTestFileServer()
|
||||
}
|
||||
|
||||
func UnwrapResult[T any](result *model.Result[T]) T {
|
||||
if result.HasError() {
|
||||
panic(result.Msg)
|
||||
}
|
||||
return result.Data
|
||||
}
|
||||
|
||||
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
@@ -14,13 +14,10 @@ const (
|
||||
)
|
||||
|
||||
type StartConfig struct {
|
||||
Network string `json:"network"`
|
||||
Address string `json:"address"`
|
||||
RefreshInterval int `json:"refreshInterval"`
|
||||
Storage Storage `json:"storage"`
|
||||
StorageDir string `json:"storageDir"`
|
||||
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||
ApiToken string `json:"apiToken"`
|
||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||
|
||||
ProductionMode bool
|
||||
@@ -31,12 +28,6 @@ type StartConfig struct {
|
||||
}
|
||||
|
||||
func (cfg *StartConfig) Init() *StartConfig {
|
||||
if cfg.Network == "" {
|
||||
cfg.Network = "tcp"
|
||||
}
|
||||
if cfg.Address == "" {
|
||||
cfg.Address = "127.0.0.1:0"
|
||||
}
|
||||
if cfg.RefreshInterval == 0 {
|
||||
cfg.RefreshInterval = 350
|
||||
}
|
||||
@@ -59,3 +50,9 @@ func (cfg *WebBasicAuth) Authorization() string {
|
||||
userId := cfg.Username + ":" + cfg.Password
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
|
||||
}
|
||||
|
||||
type HttpListenResult struct {
|
||||
Host string `json:"host"`
|
||||
// Port is the http server real listen port.
|
||||
Port int `json:"port"`
|
||||
}
|
||||
@@ -13,6 +13,6 @@ type SwitchExtension struct {
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
type UpdateCheckExtensionResp struct {
|
||||
type UpgradeCheckExtensionResp struct {
|
||||
NewVersion string `json:"newVersion"`
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type RespCode int
|
||||
|
||||
const (
|
||||
@@ -20,6 +22,15 @@ type Result[T any] struct {
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
func (r *Result[T]) Json() string {
|
||||
buf, _ := json.Marshal(r)
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func (r *Result[T]) HasError() bool {
|
||||
return r.Code != CodeOk
|
||||
}
|
||||
|
||||
func NewOkResult[T any](data T) *Result[T] {
|
||||
return &Result[T]{
|
||||
Code: CodeOk,
|
||||
@@ -33,14 +44,14 @@ func NewNilResult() *Result[any] {
|
||||
}
|
||||
}
|
||||
|
||||
func NewErrorResult(msg string, code ...RespCode) *Result[any] {
|
||||
func NewErrorResult[T any](msg string, code ...RespCode) *Result[T] {
|
||||
// if code is not provided, the default code is CodeError
|
||||
c := CodeError
|
||||
if len(code) > 0 {
|
||||
c = code[0]
|
||||
}
|
||||
|
||||
return &Result[any]{
|
||||
return &Result[T]{
|
||||
Code: c,
|
||||
Msg: msg,
|
||||
}
|
||||
File renamed without changes.
@@ -15,7 +15,6 @@ const (
|
||||
HttpCodeOK = 200
|
||||
HttpCodePartialContent = 206
|
||||
|
||||
HttpHeaderHost = "Host"
|
||||
HttpHeaderRange = "Range"
|
||||
HttpHeaderAcceptRanges = "Accept-Ranges"
|
||||
HttpHeaderContentLength = "Content-Length"
|
||||
|
||||
@@ -171,6 +171,7 @@ type DownloaderStoreConfig struct {
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||
Extra map[string]any `json:"extra"`
|
||||
Http *DownloaderHttpConfig `json:"http"`
|
||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||
}
|
||||
|
||||
@@ -181,6 +182,12 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.ProtocolConfig == nil {
|
||||
cfg.ProtocolConfig = make(map[string]any)
|
||||
}
|
||||
if cfg.Extra == nil {
|
||||
cfg.Extra = make(map[string]any)
|
||||
}
|
||||
if cfg.Http == nil {
|
||||
cfg.Http = &DownloaderHttpConfig{}
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
@@ -203,12 +210,28 @@ func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *Downl
|
||||
if cfg.Extra == nil {
|
||||
cfg.Extra = beforeCfg.Extra
|
||||
}
|
||||
if cfg.Http == nil {
|
||||
cfg.Http = beforeCfg.Http
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = beforeCfg.Proxy
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type DownloaderHttpConfig struct {
|
||||
// Enable is the flag that enable http server
|
||||
Enable bool `json:"Enable"`
|
||||
// Host is the http server listen address.
|
||||
Host string `json:"host"`
|
||||
// Port is the http server listen port.
|
||||
Port int `json:"port"`
|
||||
// ApiToken is the auth token for http API
|
||||
ApiToken string `json:"apiToken"`
|
||||
// RunningPort is the http server real listen port.
|
||||
RunningPort int `json:"runningPort"`
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
// System is the flag that use system proxy
|
||||
|
||||
@@ -17,6 +17,8 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
@@ -28,6 +30,8 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
@@ -43,6 +47,8 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
@@ -56,6 +62,8 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
},
|
||||
|
||||
+18
-15
@@ -252,7 +252,7 @@ func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fet
|
||||
ctl.GetConfig = func(v any) {
|
||||
d.getProtocolConfig(fm.Name(), v)
|
||||
}
|
||||
// Get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||
// get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
if requestProxy == nil {
|
||||
return d.cfg.Proxy.ToHandler()
|
||||
@@ -390,7 +390,7 @@ func (d *Downloader) Pause(filter *TaskFilter) (err error) {
|
||||
return d.pauseAll()
|
||||
}
|
||||
|
||||
filter.NotStatuses = []base.Status{base.DownloadStatusPause, base.DownloadStatusError, base.DownloadStatusDone}
|
||||
filter.NotStatus = []base.Status{base.DownloadStatusPause, base.DownloadStatusError, base.DownloadStatusDone}
|
||||
pauseTasks := d.GetTasksByFilter(filter)
|
||||
if len(pauseTasks) == 0 {
|
||||
return ErrTaskNotFound
|
||||
@@ -430,7 +430,7 @@ func (d *Downloader) Continue(filter *TaskFilter) (err error) {
|
||||
return d.continueAll()
|
||||
}
|
||||
|
||||
filter.NotStatuses = []base.Status{base.DownloadStatusRunning, base.DownloadStatusDone}
|
||||
filter.NotStatus = []base.Status{base.DownloadStatusRunning, base.DownloadStatusDone}
|
||||
continueTasks := d.GetTasksByFilter(filter)
|
||||
if len(continueTasks) == 0 {
|
||||
return ErrTaskNotFound
|
||||
@@ -527,7 +527,7 @@ func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
|
||||
|
||||
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||
if filter == nil || filter.IsEmpty() {
|
||||
return d.deleteAll()
|
||||
return d.deleteAll(force)
|
||||
}
|
||||
|
||||
deleteTasks := d.GetTasksByFilter(filter)
|
||||
@@ -572,24 +572,27 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) deleteAll() (err error) {
|
||||
func (d *Downloader) deleteAll(force bool) (err error) {
|
||||
deleteTasksPtr := make([]*Task, 0)
|
||||
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
deleteTasksPtr = append(deleteTasksPtr, d.tasks...)
|
||||
d.tasks = make([]*Task, 0)
|
||||
d.waitTasks = make([]*Task, 0)
|
||||
}()
|
||||
|
||||
for _, task := range d.tasks {
|
||||
if err = d.doDelete(task, true); err != nil {
|
||||
for _, task := range deleteTasksPtr {
|
||||
if err = d.doDelete(task, force); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) Stats(id string) (sr any, err error) {
|
||||
func (d *Downloader) GetTaskStats(id string) (sr any, err error) {
|
||||
task := d.GetTask(id)
|
||||
if task == nil {
|
||||
return sr, ErrTaskNotFound
|
||||
@@ -719,10 +722,10 @@ func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||
}
|
||||
|
||||
idMatch := func(task *Task) bool {
|
||||
if len(filter.IDs) == 0 {
|
||||
if len(filter.ID) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, id := range filter.IDs {
|
||||
for _, id := range filter.ID {
|
||||
if task.ID == id {
|
||||
return true
|
||||
}
|
||||
@@ -730,10 +733,10 @@ func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||
return false
|
||||
}
|
||||
statusMatch := func(task *Task) bool {
|
||||
if len(filter.Statuses) == 0 {
|
||||
if len(filter.Status) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, status := range filter.Statuses {
|
||||
for _, status := range filter.Status {
|
||||
if task.Status == status {
|
||||
return true
|
||||
}
|
||||
@@ -741,10 +744,10 @@ func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||
return false
|
||||
}
|
||||
notStatusMatch := func(task *Task) bool {
|
||||
if len(filter.NotStatuses) == 0 {
|
||||
if len(filter.NotStatus) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, status := range filter.NotStatuses {
|
||||
for _, status := range filter.NotStatus {
|
||||
if task.Status == status {
|
||||
return false
|
||||
}
|
||||
@@ -826,7 +829,7 @@ func (d *Downloader) watch(task *Task) {
|
||||
}
|
||||
|
||||
// When delete a not resolved task, need check if the task resource is nil
|
||||
if task.Meta.Res == nil || d.GetTask(task.ID) == nil {
|
||||
if task.Meta.Res == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -400,7 +400,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Millisecond * 1001)
|
||||
err = downloader.Pause(&TaskFilter{IDs: []string{id}})
|
||||
err = downloader.Pause(&TaskFilter{ID: []string{id}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -424,7 +424,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
err = downloader.Continue(&TaskFilter{IDs: []string{id}})
|
||||
err = downloader.Continue(&TaskFilter{ID: []string{id}})
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -551,7 +551,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter ids", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
IDs: taskIds,
|
||||
ID: taskIds,
|
||||
})
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), len(reqs))
|
||||
@@ -560,7 +560,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter match ids", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
IDs: []string{taskIds[0]},
|
||||
ID: []string{taskIds[0]},
|
||||
})
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 1)
|
||||
@@ -569,7 +569,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match ids", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
IDs: []string{"xxx"},
|
||||
ID: []string{"xxx"},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 0)
|
||||
@@ -578,7 +578,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
Statuses: []base.Status{base.DownloadStatusDone},
|
||||
Status: []base.Status{base.DownloadStatusDone},
|
||||
})
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), len(reqs))
|
||||
@@ -587,7 +587,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
Statuses: []base.Status{base.DownloadStatusError},
|
||||
Status: []base.Status{base.DownloadStatusError},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), 0)
|
||||
@@ -596,7 +596,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter match notStatus", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
NotStatuses: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
|
||||
NotStatus: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
|
||||
})
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("GetTasksByFilter match notStatus task got = %v, want %v", len(tasks), len(reqs))
|
||||
@@ -605,7 +605,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match notStatus", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
NotStatuses: []base.Status{base.DownloadStatusDone},
|
||||
NotStatus: []base.Status{base.DownloadStatusDone},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter not match notStatus task got = %v, want %v", len(tasks), 0)
|
||||
@@ -614,8 +614,8 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter match ids and status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
IDs: []string{taskIds[0]},
|
||||
Statuses: []base.Status{base.DownloadStatusDone},
|
||||
ID: []string{taskIds[0]},
|
||||
Status: []base.Status{base.DownloadStatusDone},
|
||||
})
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("GetTasksByFilter match ids and status task got = %v, want %v", len(tasks), 1)
|
||||
@@ -624,8 +624,8 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match ids and status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
IDs: []string{taskIds[0]},
|
||||
Statuses: []base.Status{base.DownloadStatusError},
|
||||
ID: []string{taskIds[0]},
|
||||
Status: []base.Status{base.DownloadStatusError},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter not match ids and status task got = %v, want %v", len(tasks), 0)
|
||||
|
||||
@@ -702,34 +702,38 @@ type OnDoneContext struct {
|
||||
Task *Task `json:"task"`
|
||||
}
|
||||
|
||||
// ExtensionTask is a wrapper of Task, it's used to interact with extension scripts.
|
||||
// Avoid extension scripts modifying task directly, use ExtensionTask to encapsulate task,
|
||||
// only some fields can be modified, such as request info.
|
||||
type ExtensionTask struct {
|
||||
download *Downloader
|
||||
task *Task
|
||||
|
||||
*Task
|
||||
Meta *ExtensionTaskMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// ExtensionTaskMeta restricts extension scripts to only modify request info
|
||||
type ExtensionTaskMeta struct {
|
||||
Req *base.Request `json:"req"`
|
||||
}
|
||||
|
||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||
// restricts extension scripts to only modify request info
|
||||
newTask := task.clone()
|
||||
newTask.Meta.Req = task.Meta.Req
|
||||
return &ExtensionTask{
|
||||
task: newTask,
|
||||
download: download,
|
||||
Task: newTask,
|
||||
Meta: &ExtensionTaskMeta{
|
||||
Req: task.Meta.Req,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ExtensionTask) Continue() error {
|
||||
return t.download.Continue(&TaskFilter{
|
||||
IDs: []string{t.ID},
|
||||
ID: []string{t.task.ID},
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ExtensionTask) Pause() error {
|
||||
return t.download.Pause(&TaskFilter{
|
||||
IDs: []string{t.ID},
|
||||
ID: []string{t.task.ID},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func (t *Task) Name() string {
|
||||
return t.Meta.Res.Name
|
||||
}
|
||||
|
||||
// Get the name of the first file
|
||||
// get the name of the first file
|
||||
return t.Meta.Res.Files[0].Name
|
||||
}
|
||||
|
||||
@@ -113,13 +113,13 @@ func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) i
|
||||
}
|
||||
|
||||
type TaskFilter struct {
|
||||
IDs []string
|
||||
Statuses []base.Status
|
||||
NotStatuses []base.Status
|
||||
ID []string `json:"id"`
|
||||
Status []base.Status `json:"status"`
|
||||
NotStatus []base.Status `json:"notStatus"`
|
||||
}
|
||||
|
||||
func (f *TaskFilter) IsEmpty() bool {
|
||||
return len(f.IDs) == 0 && len(f.Statuses) == 0 && len(f.NotStatuses) == 0
|
||||
return len(f.ID) == 0 && len(f.Status) == 0 && len(f.NotStatus) == 0
|
||||
}
|
||||
|
||||
type DownloaderConfig struct {
|
||||
|
||||
-389
@@ -1,389 +0,0 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func Info(w http.ResponseWriter, r *http.Request) {
|
||||
info := map[string]any{
|
||||
"version": base.Version,
|
||||
"runtime": runtime.Version(),
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"inDocker": base.InDocker == "true",
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(info))
|
||||
}
|
||||
|
||||
func Resolve(w http.ResponseWriter, r *http.Request) {
|
||||
var req base.Request
|
||||
if ReadJson(r, w, &req) {
|
||||
rr, err := Downloader.Resolve(&req)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(rr))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTask
|
||||
if ReadJson(r, w, &req) {
|
||||
var (
|
||||
taskId string
|
||||
err error
|
||||
)
|
||||
if req.Rid != "" {
|
||||
taskId, err = Downloader.Create(req.Rid, req.Opt)
|
||||
} else if req.Req != nil {
|
||||
taskId, err = Downloader.CreateDirect(req.Req, req.Opt)
|
||||
} else {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(taskId))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTaskBatch
|
||||
if ReadJson(r, w, &req) {
|
||||
if len(req.Reqs) == 0 {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(taskIds))
|
||||
}
|
||||
}
|
||||
|
||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseIdFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Pause(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func PauseTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Pause(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseIdFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Continue(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Continue(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseIdFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
force := r.FormValue("force")
|
||||
|
||||
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
force := r.FormValue("force")
|
||||
|
||||
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
if taskId == "" {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
task := Downloader.GetTask(taskId)
|
||||
if task == nil {
|
||||
WriteJson(w, model.NewErrorResult("task not found", model.CodeTaskNotFound))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(task))
|
||||
}
|
||||
|
||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
tasks := Downloader.GetTasksByFilter(filter)
|
||||
WriteJson(w, model.NewOkResult(tasks))
|
||||
}
|
||||
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJson(w, model.NewOkResult(getServerConfig()))
|
||||
}
|
||||
|
||||
func PutConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var cfg base.DownloaderStoreConfig
|
||||
if ReadJson(r, w, &cfg) {
|
||||
if err := Downloader.PutConfig(&cfg); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.InstallExtension
|
||||
if ReadJson(r, w, &req) {
|
||||
var (
|
||||
installedExt *download.Extension
|
||||
err error
|
||||
)
|
||||
if req.DevMode {
|
||||
installedExt, err = Downloader.InstallExtensionByFolder(req.URL, true)
|
||||
} else {
|
||||
installedExt, err = Downloader.InstallExtensionByGit(req.URL)
|
||||
}
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(installedExt.Identity))
|
||||
}
|
||||
}
|
||||
|
||||
func GetExtensions(w http.ResponseWriter, r *http.Request) {
|
||||
list := Downloader.GetExtensions()
|
||||
WriteJson(w, model.NewOkResult(list))
|
||||
}
|
||||
|
||||
func GetExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
ext, err := Downloader.GetExtension(identity)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(ext))
|
||||
}
|
||||
|
||||
func UpdateExtensionSettings(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
var req model.UpdateExtensionSettings
|
||||
if ReadJson(r, w, &req) {
|
||||
if err := Downloader.UpdateExtensionSettings(identity, req.Settings); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func SwitchExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
var switchExtension model.SwitchExtension
|
||||
if ReadJson(r, w, &switchExtension) {
|
||||
if err := Downloader.SwitchExtension(identity, switchExtension.Status); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DeleteExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
if err := Downloader.DeleteExtension(identity); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func UpdateCheckExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
newVersion, err := Downloader.UpgradeCheckExtension(identity)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(&model.UpdateCheckExtensionResp{
|
||||
NewVersion: newVersion,
|
||||
}))
|
||||
}
|
||||
|
||||
func UpdateExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
if err := Downloader.UpgradeExtension(identity); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DoProxy(w http.ResponseWriter, r *http.Request) {
|
||||
target := r.Header.Get("X-Target-Uri")
|
||||
if target == "" {
|
||||
writeError(w, "param invalid: X-Target-Uri")
|
||||
return
|
||||
}
|
||||
targetUrl, err := url.Parse(target)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
r.RequestURI = ""
|
||||
r.URL = targetUrl
|
||||
r.Host = targetUrl.Host
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for k, vv := range resp.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func GetStats(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
if taskId == "" {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
statsResult, err := Downloader.Stats(taskId)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(statsResult))
|
||||
}
|
||||
|
||||
func parseIdFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
if taskId == "" {
|
||||
return nil, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)
|
||||
}
|
||||
|
||||
filter := &download.TaskFilter{
|
||||
IDs: []string{taskId},
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func parseFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, model.NewErrorResult(err.Error())
|
||||
}
|
||||
|
||||
filter := &download.TaskFilter{
|
||||
IDs: r.Form["id"],
|
||||
Statuses: convertStatues(r.Form["status"]),
|
||||
NotStatuses: convertStatues(r.Form["notStatus"]),
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func convertStatues(statues []string) []base.Status {
|
||||
result := make([]base.Status, 0)
|
||||
for _, status := range statues {
|
||||
result = append(result, base.Status(status))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, msg string) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func getServerConfig() *base.DownloaderStoreConfig {
|
||||
cfg, _ := Downloader.GetConfig()
|
||||
return cfg
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package rest
|
||||
|
||||
type Config struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
srv *http.Server
|
||||
runningPort int
|
||||
|
||||
Downloader *download.Downloader
|
||||
)
|
||||
|
||||
func Start(startCfg *model.StartConfig) (port int, err error) {
|
||||
// avoid repeat start
|
||||
if srv != nil {
|
||||
return runningPort, nil
|
||||
}
|
||||
|
||||
var listener net.Listener
|
||||
srv, listener, err = BuildServer(startCfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
|
||||
port = addr.Port
|
||||
runningPort = port
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
defer func() {
|
||||
srv = nil
|
||||
}()
|
||||
|
||||
if srv != nil {
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
||||
}
|
||||
}
|
||||
if Downloader != nil {
|
||||
if err := Downloader.Close(); err != nil {
|
||||
Downloader.Logger.Warn().Err(err).Msg("close downloader failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error) {
|
||||
if startCfg == nil {
|
||||
startCfg = &model.StartConfig{}
|
||||
}
|
||||
startCfg.Init()
|
||||
|
||||
downloadCfg := &download.DownloaderConfig{
|
||||
ProductionMode: startCfg.ProductionMode,
|
||||
RefreshInterval: startCfg.RefreshInterval,
|
||||
}
|
||||
if startCfg.Storage == model.StorageBolt {
|
||||
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
||||
} else {
|
||||
downloadCfg.Storage = download.NewMemStorage()
|
||||
}
|
||||
downloadCfg.StorageDir = startCfg.StorageDir
|
||||
downloadCfg.Init()
|
||||
Downloader = download.NewDownloader(downloadCfg)
|
||||
if err := Downloader.Setup(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if startCfg.Network == "unix" {
|
||||
util.SafeRemove(startCfg.Address)
|
||||
}
|
||||
|
||||
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var r = mux.NewRouter()
|
||||
r.Methods(http.MethodGet).Path("/api/v1/info").HandlerFunc(Info)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseTasks)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueTasks)
|
||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}/stats").HandlerFunc(GetStats)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions").HandlerFunc(GetExtensions)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}").HandlerFunc(GetExtension)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/settings").HandlerFunc(UpdateExtensionSettings)
|
||||
r.Methods(http.MethodPut).Path("/api/v1/extensions/{identity}/switch").HandlerFunc(SwitchExtension)
|
||||
r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
||||
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
||||
if startCfg.WebEnable {
|
||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
|
||||
}
|
||||
|
||||
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
|
||||
r.Use(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
||||
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
|
||||
}
|
||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// recover panic
|
||||
r.Use(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
err := errors.WithStack(fmt.Errorf("%v", v))
|
||||
Downloader.Logger.Error().Stack().Err(err).Msgf("http server panic: %s %s", r.Method, r.RequestURI)
|
||||
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
|
||||
}
|
||||
}()
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
srv = &http.Server{Handler: handlers.CORS(
|
||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||
handlers.AllowedOrigins([]string{"*"}),
|
||||
)(r)}
|
||||
return srv, listener, nil
|
||||
}
|
||||
|
||||
func resolvePath(urlPath string, prefix string) (identity string, path string, err error) {
|
||||
// remove prefix
|
||||
clearPath := strings.TrimPrefix(urlPath, prefix)
|
||||
// match extension identity, eg: /fs/extensions/identity/xxx
|
||||
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
|
||||
if !reg.MatchString(clearPath) {
|
||||
err = os.ErrNotExist
|
||||
return
|
||||
}
|
||||
matched := reg.FindStringSubmatch(clearPath)
|
||||
if len(matched) != 3 {
|
||||
err = os.ErrNotExist
|
||||
return
|
||||
}
|
||||
return matched[1], matched[2], nil
|
||||
}
|
||||
|
||||
// handle task file resource
|
||||
type taskFileSystem struct {
|
||||
}
|
||||
|
||||
func (e *taskFileSystem) Open(name string) (http.File, error) {
|
||||
// get extension identity
|
||||
identity, path, err := resolvePath(name, "/fs/tasks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task := Downloader.GetTask(identity)
|
||||
if task == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return os.Open(filepath.Join(task.Meta.RootDirPath(), path))
|
||||
}
|
||||
|
||||
// handle extension file resource
|
||||
type extensionFileSystem struct {
|
||||
}
|
||||
|
||||
func (e *extensionFileSystem) Open(name string) (http.File, error) {
|
||||
// get extension identity
|
||||
identity, path, err := resolvePath(name, "/fs/extensions")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extension, err := Downloader.GetExtension(identity)
|
||||
if err != nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
extensionPath := Downloader.ExtensionPath(extension)
|
||||
return os.Open(filepath.Join(extensionPath, path))
|
||||
}
|
||||
|
||||
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func WriteJson(w http.ResponseWriter, v any) {
|
||||
WriteStatusJson(w, http.StatusOK, v)
|
||||
}
|
||||
|
||||
func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
func ParseSchema(url string) string {
|
||||
index := strings.Index(url, ":")
|
||||
// if no schema or windows path like C:\a.txt, return FILE
|
||||
if index == -1 || index == 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -74,14 +74,8 @@ typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
|
||||
/* Return type for Start */
|
||||
struct Start_return {
|
||||
GoInt r0;
|
||||
char* r1;
|
||||
};
|
||||
extern struct Start_return Start(char* cfg);
|
||||
extern void Stop();
|
||||
extern __declspec(dllexport) char* Init(char* cfg);
|
||||
extern __declspec(dllexport) char* Invoke(char* req);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:gopeed/api/model/extension.dart';
|
||||
import 'package:gopeed/api/model/install_extension.dart';
|
||||
import 'package:gopeed/api/model/switch_extension.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
import 'model/create_task.dart';
|
||||
import 'model/downloader_config.dart';
|
||||
import 'model/request.dart';
|
||||
import 'model/resolve_result.dart';
|
||||
import 'model/result.dart';
|
||||
import 'model/task.dart';
|
||||
import 'model/update_extension_settings.dart';
|
||||
import 'model/update_check_extension_resp.dart';
|
||||
|
||||
class _Client {
|
||||
static _Client? _instance;
|
||||
|
||||
late Dio dio;
|
||||
|
||||
_Client._internal();
|
||||
|
||||
factory _Client(String network, String address, String apiToken) {
|
||||
if (_instance == null) {
|
||||
_instance = _Client._internal();
|
||||
var dio = Dio();
|
||||
final isUnixSocket = network == 'unix';
|
||||
var baseUrl = 'http://127.0.0.1';
|
||||
if (!isUnixSocket) {
|
||||
if (Util.isWeb()) {
|
||||
baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : '';
|
||||
} else {
|
||||
baseUrl = 'http://$address';
|
||||
}
|
||||
}
|
||||
dio.options.baseUrl = baseUrl;
|
||||
dio.options.contentType = Headers.jsonContentType;
|
||||
dio.options.sendTimeout = const Duration(seconds: 5);
|
||||
dio.options.connectTimeout = const Duration(seconds: 5);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 60);
|
||||
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
|
||||
if (apiToken.isNotEmpty) {
|
||||
options.headers['X-Api-Token'] = apiToken;
|
||||
}
|
||||
handler.next(options);
|
||||
}));
|
||||
|
||||
_instance!.dio = dio;
|
||||
if (isUnixSocket) {
|
||||
(_instance!.dio.httpClientAdapter as IOHttpClientAdapter)
|
||||
.createHttpClient = () {
|
||||
final client = HttpClient();
|
||||
client.connectionFactory =
|
||||
(Uri uri, String? proxyHost, int? proxyPort) {
|
||||
return Socket.startConnect(
|
||||
InternetAddress(address, type: InternetAddressType.unix), 0);
|
||||
};
|
||||
return client;
|
||||
};
|
||||
}
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutException implements Exception {
|
||||
final String message;
|
||||
|
||||
TimeoutException(this.message);
|
||||
}
|
||||
|
||||
late _Client _client;
|
||||
|
||||
void init(String network, String address, String apiToken) {
|
||||
_client = _Client(network, address, apiToken);
|
||||
}
|
||||
|
||||
Future<T> _parse<T>(
|
||||
Future<Response> Function() fetch,
|
||||
T Function(dynamic json)? fromJsonT,
|
||||
) async {
|
||||
try {
|
||||
var resp = await fetch();
|
||||
fromJsonT ??= (json) => null as T;
|
||||
final result = Result<T>.fromJson(resp.data, fromJsonT);
|
||||
if (result.code == 0) {
|
||||
return result.data as T;
|
||||
} else {
|
||||
throw Exception(result);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.sendTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.connectionTimeout) {
|
||||
throw TimeoutException("request timeout");
|
||||
}
|
||||
throw Exception(Result(code: 1000, msg: e.message));
|
||||
}
|
||||
}
|
||||
|
||||
Future<ResolveResult> resolve(Request request) async {
|
||||
return _parse<ResolveResult>(
|
||||
() => _client.dio.post("/api/v1/resolve", data: request),
|
||||
(data) => ResolveResult.fromJson(data));
|
||||
}
|
||||
|
||||
Future<String> createTask(CreateTask createTask) async {
|
||||
return _parse<String>(
|
||||
() => _client.dio.post("/api/v1/tasks", data: createTask),
|
||||
(data) => data as String);
|
||||
}
|
||||
|
||||
Future<List<Task>> getTasks(List<Status> statuses) async {
|
||||
return _parse<List<Task>>(
|
||||
() => _client.dio.get(
|
||||
"/api/v1/tasks?${statuses.map((e) => "status=${e.name}").join("&")}"),
|
||||
(data) => (data as List).map((e) => Task.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
Future<void> pauseTask(String id) async {
|
||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/pause"), null);
|
||||
}
|
||||
|
||||
Future<void> continueTask(String id) async {
|
||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
||||
}
|
||||
|
||||
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||
return _parse(
|
||||
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
|
||||
"id": ids,
|
||||
}),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> continueAllTasks(List<String>? ids) async {
|
||||
return _parse(
|
||||
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
|
||||
"id": ids,
|
||||
}),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> deleteTask(String id, bool force) async {
|
||||
return _parse(
|
||||
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
||||
}
|
||||
|
||||
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
||||
return _parse(
|
||||
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
|
||||
"id": ids,
|
||||
"force": force,
|
||||
}),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<DownloaderConfig> getConfig() async {
|
||||
return _parse(() => _client.dio.get("/api/v1/config"),
|
||||
(data) => DownloaderConfig.fromJson(data));
|
||||
}
|
||||
|
||||
Future<void> putConfig(DownloaderConfig config) async {
|
||||
return _parse(() => _client.dio.put("/api/v1/config", data: config), null);
|
||||
}
|
||||
|
||||
Future<void> installExtension(InstallExtension installExtension) async {
|
||||
return _parse(
|
||||
() => _client.dio.post("/api/v1/extensions", data: installExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<List<Extension>> getExtensions() async {
|
||||
return _parse<List<Extension>>(() => _client.dio.get("/api/v1/extensions"),
|
||||
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
Future<void> updateExtensionSettings(
|
||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||
return _parse(
|
||||
() => _client.dio.put("/api/v1/extensions/$identity/settings",
|
||||
data: updateExtensionSettings),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> switchExtension(
|
||||
String identity, SwitchExtension switchExtension) async {
|
||||
return _parse(
|
||||
() => _client.dio
|
||||
.put("/api/v1/extensions/$identity/switch", data: switchExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> deleteExtension(String identity) async {
|
||||
return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null);
|
||||
}
|
||||
|
||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
||||
return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"),
|
||||
(data) => UpdateCheckExtensionResp.fromJson(data));
|
||||
}
|
||||
|
||||
Future<void> updateExtension(String identity) async {
|
||||
return _parse(
|
||||
() => _client.dio.post("/api/v1/extensions/$identity/update"), null);
|
||||
}
|
||||
|
||||
Future<Response<String>> proxyRequest<T>(String uri,
|
||||
{data, Options? options}) async {
|
||||
options ??= Options();
|
||||
options.headers ??= {};
|
||||
options.headers!["X-Target-Uri"] = uri;
|
||||
|
||||
// add timestamp to avoid cache
|
||||
return _client.dio.request(
|
||||
"/api/v1/proxy?t=${DateTime.now().millisecondsSinceEpoch}",
|
||||
data: data,
|
||||
options: options);
|
||||
}
|
||||
|
||||
String join(String path) {
|
||||
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
class ApiException implements Exception {
|
||||
final int code;
|
||||
final String message;
|
||||
|
||||
ApiException(this.code, this.message);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../api_exception.dart';
|
||||
import '../model/result.dart';
|
||||
|
||||
mixin LibgopeedBootBase {
|
||||
Dio createDio() {
|
||||
Dio dio = Dio();
|
||||
dio.options.contentType = Headers.jsonContentType;
|
||||
dio.options.sendTimeout = const Duration(seconds: 5);
|
||||
dio.options.connectTimeout = const Duration(seconds: 5);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 60);
|
||||
return dio;
|
||||
}
|
||||
|
||||
T handleResult<T>(
|
||||
Result<T> result,
|
||||
) {
|
||||
if (result.code == 0) {
|
||||
return result.data as T;
|
||||
} else {
|
||||
throw ApiException(result.code, result.msg ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../api/model/create_task.dart';
|
||||
import '../../api/model/create_task_batch.dart';
|
||||
import '../../api/model/downloader_config.dart';
|
||||
import '../../api/model/extension.dart';
|
||||
import '../../api/model/http_listen_result.dart';
|
||||
import '../../api/model/info.dart';
|
||||
import '../../api/model/install_extension.dart';
|
||||
import '../../api/model/request.dart';
|
||||
import '../../api/model/resolve_result.dart';
|
||||
import '../../api/model/result.dart';
|
||||
import '../../api/model/switch_extension.dart';
|
||||
import '../../api/model/task.dart';
|
||||
import '../../api/model/task_filter.dart';
|
||||
import '../../api/model/update_check_extension_resp.dart';
|
||||
import '../../api/model/update_extension_settings.dart';
|
||||
import '../../util/extensions.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../libgopeed_boot.dart';
|
||||
import '../native/channel/libgopeed_channel.dart';
|
||||
import '../native/ffi/libgopeed_bind.dart';
|
||||
import '../native/ffi/libgopeed_ffi.dart';
|
||||
import '../native/libgopeed_interface.dart';
|
||||
import '../native/model/invoke_request.dart';
|
||||
import '../native/model/start_config.dart';
|
||||
import 'libgopeed_boot_base.dart';
|
||||
|
||||
LibgopeedBoot create() => LibgopeedBootNative();
|
||||
|
||||
class LibgopeedBootNative
|
||||
with LibgopeedBootBase
|
||||
implements LibgopeedBoot, LibgopeedApi {
|
||||
late LibgopeedAbi _libgopeedAbi;
|
||||
late Dio _dio;
|
||||
|
||||
LibgopeedBootNative() {
|
||||
if (Util.isDesktop()) {
|
||||
var libName = "libgopeed.";
|
||||
if (Platform.isWindows) {
|
||||
libName += "dll";
|
||||
}
|
||||
if (Platform.isMacOS) {
|
||||
libName += "dylib";
|
||||
}
|
||||
if (Platform.isLinux) {
|
||||
libName += "so";
|
||||
}
|
||||
_libgopeedAbi = LibgopeedFFi(LibgopeedBind(DynamicLibrary.open(libName)));
|
||||
} else {
|
||||
_libgopeedAbi = LibgopeedChannel();
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> _invoke<T>(
|
||||
String method,
|
||||
List<dynamic> params,
|
||||
T Function(dynamic json)? fromJsonT,
|
||||
) async {
|
||||
final invokeRequest = InvokeRequest(method: method, params: params);
|
||||
final resp = await _libgopeedAbi.invoke(jsonEncode(invokeRequest));
|
||||
final result =
|
||||
Result<T>.fromJson(jsonDecode(resp), fromJsonT ??= (json) => null as T);
|
||||
return handleResult(result);
|
||||
}
|
||||
|
||||
List<dynamic> _parseTaskFilter(TaskFilter? filter) {
|
||||
if (filter == null) {
|
||||
return [null];
|
||||
}
|
||||
return [filter];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LibgopeedApi> init(StartConfig cfg) async {
|
||||
await _libgopeedAbi.init(jsonEncode(cfg.toJson()));
|
||||
_dio = createDio();
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpListenResult> startHttp() async {
|
||||
return _invoke<HttpListenResult>(
|
||||
"StartHttp", [], (json) => HttpListenResult.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopHttp() async {
|
||||
await _invoke<void>("StopHttp", [], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restartHttp() async {
|
||||
await _invoke<void>("RestartHttp", [], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Info> info() async {
|
||||
return _invoke<Info>("Info", [], (json) => Info.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ResolveResult> resolve(Request request) async {
|
||||
return _invoke<ResolveResult>(
|
||||
"Resolve", [request], (json) => ResolveResult.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> createTask(CreateTask createTask) async {
|
||||
return _invoke<String>(
|
||||
"CreateTask", [createTask], (json) => json as String);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> createTaskBatch(CreateTaskBatch createTask) async {
|
||||
return _invoke<String>(
|
||||
"CreateTaskBatch", [createTask], (json) => json as String);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseTask(String id) async {
|
||||
await _invoke<void>("PauseTask", [id], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseTasks(TaskFilter? filter) async {
|
||||
await _invoke<void>("PauseTasks", _parseTaskFilter(filter), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> continueTask(String id) async {
|
||||
await _invoke<void>("ContinueTask", [id], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> continueTasks(TaskFilter? filter) async {
|
||||
await _invoke<void>("ContinueTasks", _parseTaskFilter(filter), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTask(String id, bool force) async {
|
||||
await _invoke<void>("DeleteTask", [id, force], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTasks(TaskFilter? filter, bool force) async {
|
||||
await _invoke<void>(
|
||||
"DeleteTasks",
|
||||
_parseTaskFilter(filter).apply((it) {
|
||||
it.add(force);
|
||||
}),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Task> getTask(String id) async {
|
||||
return _invoke<Task>("GetTask", [id], (json) => Task.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> getTasks(TaskFilter? filter) async {
|
||||
return _invoke<List<Task>>("GetTasks", _parseTaskFilter(filter),
|
||||
(json) => (json as List).map((e) => Task.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getTaskStats(String id) async {
|
||||
return _invoke<Map<String, dynamic>>(
|
||||
"GetTaskStats", [id], (json) => json as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DownloaderConfig> getConfig() async {
|
||||
return _invoke<DownloaderConfig>(
|
||||
"GetConfig", [], (json) => DownloaderConfig.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> putConfig(DownloaderConfig config) async {
|
||||
await _invoke<void>("PutConfig", [config], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> installExtension(InstallExtension installExtension) async {
|
||||
await _invoke<void>("InstallExtension", [installExtension], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Extension>> getExtensions() async {
|
||||
return _invoke<List<Extension>>("GetExtensions", [],
|
||||
(json) => (json as List).map((e) => Extension.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateExtensionSettings(
|
||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||
await _invoke<void>(
|
||||
"UpdateExtensionSettings", [identity, updateExtensionSettings], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> switchExtension(
|
||||
String identity, SwitchExtension switchExtension) async {
|
||||
await _invoke<void>("SwitchExtension", [identity, switchExtension], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteExtension(String identity) async {
|
||||
await _invoke<void>("DeleteExtension", [identity], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(
|
||||
String identity) async {
|
||||
return _invoke<UpdateCheckExtensionResp>("UpgradeCheckExtension",
|
||||
[identity], (json) => UpdateCheckExtensionResp.fromJson(json));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> upgradeExtension(String identity) async {
|
||||
await _invoke<void>("UpgradeExtension", [identity], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _invoke<void>("Close", [], null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<String>> proxyRequest<T>(String uri,
|
||||
{data, Options? options}) {
|
||||
return _dio.request(uri, data: data, options: options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../api/model/create_task.dart';
|
||||
import '../../api/model/create_task_batch.dart';
|
||||
import '../../api/model/downloader_config.dart';
|
||||
import '../../api/model/extension.dart';
|
||||
import '../../api/model/http_listen_result.dart';
|
||||
import '../../api/model/info.dart';
|
||||
import '../../api/model/install_extension.dart';
|
||||
import '../../api/model/request.dart';
|
||||
import '../../api/model/resolve_result.dart';
|
||||
import '../../api/model/result.dart';
|
||||
import '../../api/model/switch_extension.dart';
|
||||
import '../../api/model/task.dart';
|
||||
import '../../api/model/task_filter.dart';
|
||||
import '../../api/model/update_check_extension_resp.dart';
|
||||
import '../../api/model/update_extension_settings.dart';
|
||||
import '../../util/extensions.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../api_exception.dart';
|
||||
import '../libgopeed_boot.dart';
|
||||
import '../native/libgopeed_interface.dart';
|
||||
import '../native/model/start_config.dart';
|
||||
import 'libgopeed_boot_base.dart';
|
||||
|
||||
LibgopeedBoot create() => LibgopeedBootWeb();
|
||||
|
||||
class LibgopeedBootWeb
|
||||
with LibgopeedBootBase
|
||||
implements LibgopeedBoot, LibgopeedApi {
|
||||
late Dio _dio;
|
||||
static const String _apiPrefix = '/api/v1';
|
||||
|
||||
LibgopeedBootWeb();
|
||||
|
||||
Future<T> _fetch<T>(
|
||||
Future<Response> Function() fetch,
|
||||
T Function(dynamic json)? fromJsonT,
|
||||
) async {
|
||||
try {
|
||||
var resp = await fetch();
|
||||
fromJsonT ??= (json) => null as T;
|
||||
final result = Result<T>.fromJson(resp.data, fromJsonT);
|
||||
return handleResult(result);
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.sendTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.connectionTimeout) {
|
||||
throw ApiException(1000, 'request timeout');
|
||||
}
|
||||
throw ApiException(1000, e.message ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _parseTaskFilter(TaskFilter? filter) {
|
||||
if (filter == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> params = {};
|
||||
if (filter.id != null) {
|
||||
params['id'] = filter.id;
|
||||
}
|
||||
if (filter.status != null) {
|
||||
params['status'] = filter.status;
|
||||
}
|
||||
if (filter.notStatus != null) {
|
||||
params['notStatus'] = filter.notStatus;
|
||||
}
|
||||
if (params.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LibgopeedApi> init(StartConfig cfg) async {
|
||||
_dio = createDio();
|
||||
_dio.options.baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : '';
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpListenResult> startHttp() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopHttp() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restartHttp() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Info> info() {
|
||||
return _fetch<Info>(
|
||||
() => _dio.get("$_apiPrefix/info"), (data) => Info.fromJson(data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ResolveResult> resolve(Request request) async {
|
||||
return _fetch<ResolveResult>(
|
||||
() => _dio.post("$_apiPrefix/resolve", data: request),
|
||||
(data) => ResolveResult.fromJson(data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> createTask(CreateTask createTask) async {
|
||||
return _fetch<String>(
|
||||
() => _dio.post("$_apiPrefix/tasks", data: createTask),
|
||||
(data) => data as String);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> createTaskBatch(CreateTaskBatch createTask) async {
|
||||
return _fetch<String>(
|
||||
() => _dio.post("$_apiPrefix/tasks", data: createTask),
|
||||
(data) => data as String);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseTask(String id) async {
|
||||
await _fetch<void>(() => _dio.put("$_apiPrefix/tasks/$id/pause"), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseTasks(TaskFilter? filter) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.put("$_apiPrefix/tasks/pause",
|
||||
queryParameters: _parseTaskFilter(filter)),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> continueTask(String id) async {
|
||||
await _fetch<void>(() => _dio.put("$_apiPrefix/tasks/$id/continue"), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> continueTasks(TaskFilter? filter) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.put("$_apiPrefix/tasks/continue",
|
||||
queryParameters: _parseTaskFilter(filter)),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTask(String id, bool force) async {
|
||||
await _fetch<void>(
|
||||
() => _dio
|
||||
.delete("$_apiPrefix/tasks/$id", queryParameters: {'force': force}),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteTasks(TaskFilter? filter, bool force) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.delete("$_apiPrefix/tasks",
|
||||
queryParameters: _parseTaskFilter(filter)?.apply((it) {
|
||||
it['force'] = force;
|
||||
})),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Task> getTask(String id) async {
|
||||
return _fetch<Task>(
|
||||
() => _dio.get("$_apiPrefix/tasks/$id"), (data) => Task.fromJson(data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Task>> getTasks(TaskFilter? filter) async {
|
||||
return _fetch<List<Task>>(
|
||||
() => _dio.get("$_apiPrefix/tasks",
|
||||
queryParameters: _parseTaskFilter(filter)),
|
||||
(data) => (data as List).map((e) => Task.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getTaskStats(String id) async {
|
||||
return _fetch<Map<String, dynamic>>(
|
||||
() => _dio.get("$_apiPrefix/tasks/$id/stats"),
|
||||
(data) => data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DownloaderConfig> getConfig() async {
|
||||
return _fetch<DownloaderConfig>(() => _dio.get("$_apiPrefix/config"),
|
||||
(data) => DownloaderConfig.fromJson(data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> putConfig(DownloaderConfig config) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.put("$_apiPrefix/config", data: config), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> installExtension(InstallExtension installExtension) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.post("$_apiPrefix/extensions", data: installExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Extension>> getExtensions() async {
|
||||
return _fetch<List<Extension>>(() => _dio.get("$_apiPrefix/extensions"),
|
||||
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateExtensionSettings(
|
||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.put("$_apiPrefix/extensions/$identity/settings",
|
||||
data: updateExtensionSettings),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> switchExtension(
|
||||
String identity, SwitchExtension switchExtension) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.put("$_apiPrefix/extensions/$identity/switch",
|
||||
data: switchExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteExtension(String identity) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.delete("$_apiPrefix/extensions/$identity"), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(
|
||||
String identity) async {
|
||||
return _fetch<UpdateCheckExtensionResp>(
|
||||
() => _dio.get("$_apiPrefix/extensions/$identity/upgrade"),
|
||||
(data) => UpdateCheckExtensionResp.fromJson(data));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> upgradeExtension(String identity) async {
|
||||
await _fetch<void>(
|
||||
() => _dio.post("$_apiPrefix/extensions/$identity/upgrade"), null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
// To avoid CORS, use a backend proxy to forward the request
|
||||
@override
|
||||
Future<Response<String>> proxyRequest<T>(String uri,
|
||||
{data, Options? options}) async {
|
||||
options ??= Options();
|
||||
options.headers ??= {};
|
||||
options.headers!["X-Target-Uri"] = uri;
|
||||
|
||||
// add timestamp to avoid cache
|
||||
return _dio.request(
|
||||
"/api/v1/proxy?t=${DateTime.now().millisecondsSinceEpoch}",
|
||||
data: data,
|
||||
options: options);
|
||||
}
|
||||
|
||||
String join(String path) {
|
||||
return "${_dio.options.baseUrl}/${Util.cleanPath(path)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import "libgopeed_boot_stub.dart"
|
||||
if (dart.library.html) 'entry/libgopeed_boot_browser.dart'
|
||||
if (dart.library.io) 'entry/libgopeed_boot_native.dart';
|
||||
import 'native/libgopeed_interface.dart';
|
||||
|
||||
abstract class LibgopeedBoot implements LibgopeedApiSingleton {
|
||||
static LibgopeedApi? _instance;
|
||||
|
||||
static void singleton(LibgopeedApi instance) {
|
||||
_instance ??= instance;
|
||||
}
|
||||
|
||||
static LibgopeedApi get instance {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
factory LibgopeedBoot() => create();
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,20 @@
|
||||
import 'package:gopeed/api/model/extension.dart';
|
||||
import 'package:gopeed/api/model/request.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'create_task_batch.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class CreateTaskBatch {
|
||||
List<Request> reqs;
|
||||
Option? opt;
|
||||
|
||||
CreateTaskBatch({
|
||||
this.reqs = const [],
|
||||
this.opt,
|
||||
});
|
||||
|
||||
factory CreateTaskBatch.fromJson(Map<String, dynamic> json) =>
|
||||
_$CreateTaskBatchFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'create_task_batch.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CreateTaskBatch _$CreateTaskBatchFromJson(Map<String, dynamic> json) =>
|
||||
CreateTaskBatch(
|
||||
reqs: (json['reqs'] as List<dynamic>?)
|
||||
?.map((e) => Request.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
opt: json['opt'] == null
|
||||
? null
|
||||
: Option.fromJson(json['opt'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
|
||||
final val = <String, dynamic>{
|
||||
'reqs': instance.reqs,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('opt', instance.opt);
|
||||
return val;
|
||||
}
|
||||
@@ -80,7 +80,6 @@ class ExtraConfig {
|
||||
String locale;
|
||||
bool lastDeleteTaskKeep;
|
||||
bool defaultDirectDownload;
|
||||
bool defaultBtClient;
|
||||
|
||||
ExtraConfigBt bt = ExtraConfigBt();
|
||||
|
||||
@@ -89,7 +88,6 @@ class ExtraConfig {
|
||||
this.locale = '',
|
||||
this.lastDeleteTaskKeep = false,
|
||||
this.defaultDirectDownload = false,
|
||||
this.defaultBtClient = true,
|
||||
});
|
||||
|
||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
|
||||
@@ -73,7 +73,6 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
locale: json['locale'] as String? ?? '',
|
||||
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
||||
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
||||
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
|
||||
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
@@ -82,7 +81,6 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
'locale': instance.locale,
|
||||
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||
'defaultBtClient': instance.defaultBtClient,
|
||||
'bt': instance.bt.toJson(),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'http_listen_result.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class HttpListenResult {
|
||||
String host;
|
||||
int port;
|
||||
|
||||
HttpListenResult({
|
||||
this.host = '',
|
||||
this.port = 0,
|
||||
});
|
||||
|
||||
factory HttpListenResult.fromJson(Map<String, dynamic> json) =>
|
||||
_$HttpListenResultFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$HttpListenResultToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'http_listen_result.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
HttpListenResult _$HttpListenResultFromJson(Map<String, dynamic> json) =>
|
||||
HttpListenResult(
|
||||
host: json['host'] as String? ?? '',
|
||||
port: (json['port'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HttpListenResultToJson(HttpListenResult instance) =>
|
||||
<String, dynamic>{
|
||||
'host': instance.host,
|
||||
'port': instance.port,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'info.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Info {
|
||||
String version;
|
||||
String runtime;
|
||||
String os;
|
||||
String arch;
|
||||
bool inDocker;
|
||||
|
||||
Info({
|
||||
this.version = '',
|
||||
this.runtime = '',
|
||||
this.os = '',
|
||||
this.arch = '',
|
||||
this.inDocker = false,
|
||||
});
|
||||
|
||||
factory Info.fromJson(Map<String, dynamic> json) => _$InfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$InfoToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Info _$InfoFromJson(Map<String, dynamic> json) => Info(
|
||||
version: json['version'] as String? ?? '',
|
||||
runtime: json['runtime'] as String? ?? '',
|
||||
os: json['os'] as String? ?? '',
|
||||
arch: json['arch'] as String? ?? '',
|
||||
inDocker: json['inDocker'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InfoToJson(Info instance) => <String, dynamic>{
|
||||
'version': instance.version,
|
||||
'runtime': instance.runtime,
|
||||
'os': instance.os,
|
||||
'arch': instance.arch,
|
||||
'inDocker': instance.inDocker,
|
||||
};
|
||||
@@ -10,8 +10,8 @@ class Options {
|
||||
Object? extra;
|
||||
|
||||
Options({
|
||||
this.name = '',
|
||||
this.path = '',
|
||||
required this.name,
|
||||
required this.path,
|
||||
this.selectFiles = const [],
|
||||
this.extra,
|
||||
});
|
||||
@@ -24,16 +24,12 @@ class Options {
|
||||
|
||||
@JsonSerializable()
|
||||
class OptsExtraHttp {
|
||||
int connections;
|
||||
bool autoTorrent;
|
||||
int connections = 0;
|
||||
bool autoTorrent = false;
|
||||
|
||||
OptsExtraHttp({
|
||||
this.connections = 0,
|
||||
this.autoTorrent = false,
|
||||
});
|
||||
OptsExtraHttp();
|
||||
|
||||
factory OptsExtraHttp.fromJson(Map<String, dynamic> json) =>
|
||||
_$OptsExtraHttpFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this);
|
||||
}
|
||||
@@ -7,8 +7,8 @@ part of 'options.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
|
||||
name: json['name'] as String? ?? '',
|
||||
path: json['path'] as String? ?? '',
|
||||
name: json['name'] as String,
|
||||
path: json['path'] as String,
|
||||
selectFiles: (json['selectFiles'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList() ??
|
||||
@@ -34,10 +34,9 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
|
||||
}
|
||||
|
||||
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
|
||||
OptsExtraHttp(
|
||||
connections: (json['connections'] as num?)?.toInt() ?? 0,
|
||||
autoTorrent: json['autoTorrent'] as bool? ?? false,
|
||||
);
|
||||
OptsExtraHttp()
|
||||
..connections = (json['connections'] as num).toInt()
|
||||
..autoTorrent = json['autoTorrent'] as bool;
|
||||
|
||||
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
|
||||
<String, dynamic>{
|
||||
|
||||
@@ -6,9 +6,9 @@ part 'request.g.dart';
|
||||
class Request {
|
||||
String url;
|
||||
Object? extra;
|
||||
Map<String, String>? labels;
|
||||
Map<String, String>? labels = {};
|
||||
RequestProxy? proxy;
|
||||
bool skipVerifyCert;
|
||||
bool skipVerifyCert = false;
|
||||
|
||||
Request({
|
||||
required this.url,
|
||||
@@ -26,15 +26,11 @@ class Request {
|
||||
|
||||
@JsonSerializable()
|
||||
class ReqExtraHttp {
|
||||
String method;
|
||||
Map<String, String> header;
|
||||
String body;
|
||||
String method = 'GET';
|
||||
Map<String, String> header = {};
|
||||
String body = '';
|
||||
|
||||
ReqExtraHttp({
|
||||
this.method = 'GET',
|
||||
this.header = const {},
|
||||
this.body = '',
|
||||
});
|
||||
ReqExtraHttp();
|
||||
|
||||
factory ReqExtraHttp.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReqExtraHttpFromJson(json);
|
||||
@@ -44,11 +40,9 @@ class ReqExtraHttp {
|
||||
|
||||
@JsonSerializable()
|
||||
class ReqExtraBt {
|
||||
List<String> trackers;
|
||||
List<String> trackers = [];
|
||||
|
||||
ReqExtraBt({
|
||||
this.trackers = const [],
|
||||
});
|
||||
ReqExtraBt();
|
||||
|
||||
factory ReqExtraBt.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReqExtraBtFromJson(json);
|
||||
@@ -64,22 +58,15 @@ enum RequestProxyMode {
|
||||
|
||||
@JsonSerializable()
|
||||
class RequestProxy {
|
||||
RequestProxyMode mode;
|
||||
String scheme;
|
||||
String host;
|
||||
String usr;
|
||||
String pwd;
|
||||
RequestProxyMode mode = RequestProxyMode.follow;
|
||||
String scheme = 'http';
|
||||
String host = '';
|
||||
String usr = '';
|
||||
String pwd = '';
|
||||
|
||||
RequestProxy({
|
||||
this.mode = RequestProxyMode.follow,
|
||||
this.scheme = 'http',
|
||||
this.host = '',
|
||||
this.usr = '',
|
||||
this.pwd = '',
|
||||
});
|
||||
RequestProxy();
|
||||
|
||||
factory RequestProxy.fromJson(Map<String, dynamic> json) =>
|
||||
_$RequestProxyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$RequestProxyToJson(this);
|
||||
}
|
||||
@@ -36,14 +36,10 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
return val;
|
||||
}
|
||||
|
||||
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp(
|
||||
method: json['method'] as String? ?? 'GET',
|
||||
header: (json['header'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
) ??
|
||||
const {},
|
||||
body: json['body'] as String? ?? '',
|
||||
);
|
||||
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp()
|
||||
..method = json['method'] as String
|
||||
..header = Map<String, String>.from(json['header'] as Map)
|
||||
..body = json['body'] as String;
|
||||
|
||||
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -52,26 +48,21 @@ Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
|
||||
'body': instance.body,
|
||||
};
|
||||
|
||||
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt(
|
||||
trackers: (json['trackers'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt()
|
||||
..trackers =
|
||||
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
|
||||
|
||||
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
|
||||
<String, dynamic>{
|
||||
'trackers': instance.trackers,
|
||||
};
|
||||
|
||||
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy(
|
||||
mode: $enumDecodeNullable(_$RequestProxyModeEnumMap, json['mode']) ??
|
||||
RequestProxyMode.follow,
|
||||
scheme: json['scheme'] as String? ?? 'http',
|
||||
host: json['host'] as String? ?? '',
|
||||
usr: json['usr'] as String? ?? '',
|
||||
pwd: json['pwd'] as String? ?? '',
|
||||
);
|
||||
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy()
|
||||
..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode'])
|
||||
..scheme = json['scheme'] as String
|
||||
..host = json['host'] as String
|
||||
..usr = json['usr'] as String
|
||||
..pwd = json['pwd'] as String;
|
||||
|
||||
Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) =>
|
||||
<String, dynamic>{
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:gopeed/api/model/task.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'task_filter.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class TaskFilter {
|
||||
List<String>? id;
|
||||
List<Status>? status;
|
||||
List<Status>? notStatus;
|
||||
|
||||
TaskFilter({this.id, this.status, this.notStatus});
|
||||
|
||||
factory TaskFilter.fromJson(Map<String, dynamic> json) =>
|
||||
_$TaskFilterFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TaskFilterToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'task_filter.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
TaskFilter _$TaskFilterFromJson(Map<String, dynamic> json) => TaskFilter(
|
||||
id: (json['ids'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
status: (json['statuses'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$StatusEnumMap, e))
|
||||
.toList(),
|
||||
notStatus: (json['notStatuses'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$StatusEnumMap, e))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TaskFilterToJson(TaskFilter instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('ids', instance.id);
|
||||
writeNotNull(
|
||||
'statuses', instance.status?.map((e) => _$StatusEnumMap[e]!).toList());
|
||||
writeNotNull('notStatuses',
|
||||
instance.notStatus?.map((e) => _$StatusEnumMap[e]!).toList());
|
||||
return val;
|
||||
}
|
||||
|
||||
const _$StatusEnumMap = {
|
||||
Status.ready: 'ready',
|
||||
Status.running: 'running',
|
||||
Status.pause: 'pause',
|
||||
Status.wait: 'wait',
|
||||
Status.error: 'error',
|
||||
Status.done: 'done',
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../libgopeed_interface.dart';
|
||||
|
||||
class LibgopeedChannel implements LibgopeedAbi {
|
||||
static const _channel = MethodChannel('gopeed.com/libgopeed');
|
||||
|
||||
@override
|
||||
Future<void> init(String cfg) async {
|
||||
await _channel.invokeMethod<String>('init', {
|
||||
'cfg': cfg,
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> invoke(String params) async {
|
||||
final result = await _channel.invokeMethod<String>('invoke', {
|
||||
'params': params,
|
||||
});
|
||||
return result!;
|
||||
}
|
||||
}
|
||||
+26
-22
@@ -857,27 +857,33 @@ class LibgopeedBind {
|
||||
late final __FCmulcr =
|
||||
__FCmulcrPtr.asFunction<_Fcomplex Function(_Fcomplex, double)>();
|
||||
|
||||
Start_return Start(
|
||||
ffi.Pointer<ffi.Char> Init(
|
||||
ffi.Pointer<ffi.Char> cfg,
|
||||
) {
|
||||
return _Start(
|
||||
return _Init(
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
late final _StartPtr =
|
||||
_lookup<ffi.NativeFunction<Start_return Function(ffi.Pointer<ffi.Char>)>>(
|
||||
'Start');
|
||||
late final _Start =
|
||||
_StartPtr.asFunction<Start_return Function(ffi.Pointer<ffi.Char>)>();
|
||||
late final _InitPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('Init');
|
||||
late final _Init = _InitPtr.asFunction<
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||
|
||||
void Stop() {
|
||||
return _Stop();
|
||||
ffi.Pointer<ffi.Char> Invoke(
|
||||
ffi.Pointer<ffi.Char> req,
|
||||
) {
|
||||
return _Invoke(
|
||||
req,
|
||||
);
|
||||
}
|
||||
|
||||
late final _StopPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function()>>('Stop');
|
||||
late final _Stop = _StopPtr.asFunction<void Function()>();
|
||||
late final _InvokePtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('Invoke');
|
||||
late final _Invoke = _InvokePtr.asFunction<
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
typedef va_list = ffi.Pointer<ffi.Char>;
|
||||
@@ -914,6 +920,7 @@ final class _Mbstatet extends ffi.Struct {
|
||||
}
|
||||
|
||||
typedef errno_t = ffi.Int;
|
||||
typedef Darterrno_t = int;
|
||||
|
||||
final class _GoString_ extends ffi.Struct {
|
||||
external ffi.Pointer<ffi.Char> p;
|
||||
@@ -923,6 +930,7 @@ final class _GoString_ extends ffi.Struct {
|
||||
}
|
||||
|
||||
typedef ptrdiff_t = ffi.LongLong;
|
||||
typedef Dartptrdiff_t = int;
|
||||
|
||||
final class _C_double_complex extends ffi.Struct {
|
||||
@ffi.Array.multi([2])
|
||||
@@ -957,13 +965,7 @@ final class GoSlice extends ffi.Struct {
|
||||
|
||||
typedef GoInt = GoInt64;
|
||||
typedef GoInt64 = ffi.LongLong;
|
||||
|
||||
final class Start_return extends ffi.Struct {
|
||||
@GoInt()
|
||||
external int r0;
|
||||
|
||||
external ffi.Pointer<ffi.Char> r1;
|
||||
}
|
||||
typedef DartGoInt64 = int;
|
||||
|
||||
const int _VCRT_COMPILER_PREPROCESSOR = 1;
|
||||
|
||||
@@ -993,6 +995,10 @@ const int _HAS_CXX23 = 0;
|
||||
|
||||
const int _HAS_NODISCARD = 1;
|
||||
|
||||
const int _ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE = 1;
|
||||
|
||||
const int _CRT_BUILD_DESKTOP_APP = 1;
|
||||
|
||||
const int _UCRT_DISABLED_WARNINGS = 4324;
|
||||
|
||||
const int _ARGMAX = 100;
|
||||
@@ -1009,9 +1015,7 @@ const int _CRT_FUNCTIONS_REQUIRED = 1;
|
||||
|
||||
const int _CRT_HAS_CXX17 = 0;
|
||||
|
||||
const int _ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE = 1;
|
||||
|
||||
const int _CRT_BUILD_DESKTOP_APP = 1;
|
||||
const int _CRT_HAS_C11 = 0;
|
||||
|
||||
const int _CRT_INTERNAL_NONSTDC_NAMES = 1;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
import '../libgopeed_interface.dart';
|
||||
import 'libgopeed_bind.dart';
|
||||
|
||||
class LibgopeedFFi implements LibgopeedAbi {
|
||||
late LibgopeedBind _libgopeed;
|
||||
|
||||
LibgopeedFFi(LibgopeedBind libgopeed) {
|
||||
_libgopeed = libgopeed;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> init(String cfg) {
|
||||
final cfgPtr = cfg.toNativeUtf8();
|
||||
try {
|
||||
final result = _libgopeed.Init(cfgPtr.cast());
|
||||
if (result != nullptr) {
|
||||
throw Exception(
|
||||
'Libgopeed init failed: ${result.cast<Utf8>().toDartString()}');
|
||||
}
|
||||
return Future.value();
|
||||
} finally {
|
||||
calloc.free(cfgPtr);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> invoke(String params) {
|
||||
final paramsPtr = params.toNativeUtf8();
|
||||
try {
|
||||
final result = _libgopeed.Invoke(paramsPtr.cast());
|
||||
return Future.value(result.cast<Utf8>().toDartString());
|
||||
} finally {
|
||||
calloc.free(paramsPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../model/create_task.dart';
|
||||
import '../model/create_task_batch.dart';
|
||||
import '../model/downloader_config.dart';
|
||||
import '../model/extension.dart';
|
||||
import '../model/http_listen_result.dart';
|
||||
import '../model/info.dart';
|
||||
import '../model/install_extension.dart';
|
||||
import '../model/request.dart';
|
||||
import '../model/resolve_result.dart';
|
||||
import '../model/switch_extension.dart';
|
||||
import '../model/task.dart';
|
||||
import '../model/task_filter.dart';
|
||||
import '../model/update_check_extension_resp.dart';
|
||||
import '../model/update_extension_settings.dart';
|
||||
import 'model/start_config.dart';
|
||||
|
||||
abstract class LibgopeedAbi {
|
||||
Future<void> init(String cfg);
|
||||
Future<String> invoke(String params);
|
||||
}
|
||||
|
||||
abstract class LibgopeedApiSingleton {
|
||||
Future<LibgopeedApi> init(StartConfig config);
|
||||
}
|
||||
|
||||
abstract class LibgopeedApi {
|
||||
Future<HttpListenResult> startHttp();
|
||||
|
||||
Future<void> stopHttp();
|
||||
|
||||
Future<void> restartHttp();
|
||||
|
||||
Future<Info> info();
|
||||
|
||||
Future<ResolveResult> resolve(Request request);
|
||||
|
||||
Future<String> createTask(CreateTask createTask);
|
||||
|
||||
Future<String> createTaskBatch(CreateTaskBatch createTask);
|
||||
|
||||
Future<void> pauseTask(String id);
|
||||
|
||||
Future<void> pauseTasks(TaskFilter? filter);
|
||||
|
||||
Future<void> continueTask(String id);
|
||||
|
||||
Future<void> continueTasks(TaskFilter? filter);
|
||||
|
||||
Future<void> deleteTask(String id, bool force);
|
||||
|
||||
Future<void> deleteTasks(TaskFilter? filter, bool force);
|
||||
|
||||
Future<Task> getTask(String id);
|
||||
|
||||
Future<List<Task>> getTasks(TaskFilter? filter);
|
||||
|
||||
Future<Map<String, dynamic>> getTaskStats(String id);
|
||||
|
||||
Future<DownloaderConfig> getConfig();
|
||||
|
||||
Future<void> putConfig(DownloaderConfig config);
|
||||
|
||||
Future<void> installExtension(InstallExtension installExtension);
|
||||
|
||||
Future<List<Extension>> getExtensions();
|
||||
|
||||
Future<void> updateExtensionSettings(
|
||||
String identity, UpdateExtensionSettings updateExtensionSettings);
|
||||
|
||||
Future<void> switchExtension(
|
||||
String identity, SwitchExtension switchExtension);
|
||||
|
||||
Future<void> deleteExtension(String identity);
|
||||
|
||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity);
|
||||
|
||||
Future<void> upgradeExtension(String identity);
|
||||
|
||||
Future<void> close();
|
||||
|
||||
Future<Response<String>> proxyRequest<T>(String uri,
|
||||
{data, Options? options});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'invoke_request.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class InvokeRequest {
|
||||
String method;
|
||||
List<dynamic> params;
|
||||
|
||||
InvokeRequest({
|
||||
this.method = '',
|
||||
this.params = const [],
|
||||
});
|
||||
|
||||
factory InvokeRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$InvokeRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$InvokeRequestToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'invoke_request.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
InvokeRequest _$InvokeRequestFromJson(Map<String, dynamic> json) =>
|
||||
InvokeRequest(
|
||||
method: json['method'] as String? ?? '',
|
||||
params: (json['params'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InvokeRequestToJson(InvokeRequest instance) =>
|
||||
<String, dynamic>{
|
||||
'method': instance.method,
|
||||
'params': instance.params,
|
||||
};
|
||||
+6
-7
@@ -4,14 +4,13 @@ part 'start_config.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class StartConfig {
|
||||
late String network;
|
||||
late String address;
|
||||
late String storage;
|
||||
late String storageDir;
|
||||
late int refreshInterval;
|
||||
late String apiToken;
|
||||
String storage;
|
||||
String storageDir;
|
||||
|
||||
StartConfig();
|
||||
StartConfig({
|
||||
this.storage = '',
|
||||
this.storageDir = '',
|
||||
});
|
||||
|
||||
factory StartConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$StartConfigFromJson(json);
|
||||
+4
-11
@@ -6,20 +6,13 @@ part of 'start_config.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
|
||||
..network = json['network'] as String
|
||||
..address = json['address'] as String
|
||||
..storage = json['storage'] as String
|
||||
..storageDir = json['storageDir'] as String
|
||||
..refreshInterval = (json['refreshInterval'] as num).toInt()
|
||||
..apiToken = json['apiToken'] as String;
|
||||
StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig(
|
||||
storage: json['storage'] as String? ?? '',
|
||||
storageDir: json['storageDir'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'network': instance.network,
|
||||
'address': instance.address,
|
||||
'storage': instance.storage,
|
||||
'storageDir': instance.storageDir,
|
||||
'refreshInterval': instance.refreshInterval,
|
||||
'apiToken': instance.apiToken,
|
||||
};
|
||||
@@ -14,11 +14,8 @@ import 'package:uri_to_file/uri_to_file.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../core/common/start_config.dart';
|
||||
import '../../../../core/libgopeed_boot.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../database/entity.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
@@ -28,9 +25,6 @@ import '../../../../util/log_util.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../create/controllers/create_controller.dart';
|
||||
import '../../create/dto/create_router_params.dart';
|
||||
import '../../redirect/views/redirect_view.dart';
|
||||
|
||||
const unixSocketPath = 'gopeed.sock';
|
||||
|
||||
@@ -78,12 +72,9 @@ final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||
});
|
||||
|
||||
class AppController extends GetxController with WindowListener, TrayListener {
|
||||
static StartConfig? _defaultStartConfig;
|
||||
|
||||
final autoStartup = false.obs;
|
||||
final startConfig = StartConfig().obs;
|
||||
final runningPort = 0.obs;
|
||||
final downloaderConfig = DownloaderConfig().obs;
|
||||
final httpRunningPort = 0.obs;
|
||||
|
||||
late AppLinks _appLinks;
|
||||
StreamSubscription<Uri>? _linkSubscription;
|
||||
@@ -115,7 +106,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
void onClose() {
|
||||
_linkSubscription?.cancel();
|
||||
trayManager.removeListener(this);
|
||||
LibgopeedBoot.instance.stop();
|
||||
LibgopeedBoot.instance.close();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -164,7 +155,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<void> _initDeepLinks() async {
|
||||
if (Util.isWeb()) {
|
||||
// currently only support android
|
||||
if (!Util.isAndroid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,13 +164,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
// Handle link when app is in warm state (front or background)
|
||||
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
|
||||
await _handleDeepLink(uri);
|
||||
await _toCreate(uri);
|
||||
});
|
||||
|
||||
// Check initial link if app was in cold state (terminated)
|
||||
final uri = await _appLinks.getInitialLink();
|
||||
final uri = await _appLinks.getInitialAppLink();
|
||||
if (uri != null) {
|
||||
await _handleDeepLink(uri);
|
||||
await _toCreate(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,11 +213,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
),
|
||||
MenuItem(
|
||||
label: "startAll".tr,
|
||||
onClick: (menuItem) async => {continueAllTasks(null)},
|
||||
onClick: (menuItem) async =>
|
||||
{LibgopeedBoot.instance.continueTasks(null)},
|
||||
),
|
||||
MenuItem(
|
||||
label: "pauseAll".tr,
|
||||
onClick: (menuItem) async => {pauseAllTasks(null)},
|
||||
onClick: (menuItem) async => {LibgopeedBoot.instance.pauseTasks(null)},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'setting'.tr,
|
||||
@@ -250,7 +243,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
label: 'exit'.tr,
|
||||
onClick: (menuItem) async {
|
||||
try {
|
||||
await LibgopeedBoot.instance.stop();
|
||||
await LibgopeedBoot.instance.close();
|
||||
} catch (e) {
|
||||
logger.w("libgopeed stop fail", e);
|
||||
}
|
||||
@@ -307,77 +300,18 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleDeepLink(Uri uri) async {
|
||||
if (uri.scheme == "gopeed") {
|
||||
if (uri.path == "/create") {
|
||||
final params = uri.queryParameters["params"];
|
||||
if (params?.isNotEmpty == true) {
|
||||
final paramsJson = String.fromCharCodes(base64Decode(params!));
|
||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||
arguments: RedirectArgs(Routes.CREATE,
|
||||
arguments:
|
||||
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
|
||||
return;
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.CREATE);
|
||||
return;
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.HOME);
|
||||
return;
|
||||
}
|
||||
|
||||
String path;
|
||||
if (uri.scheme == "magnet" ||
|
||||
uri.scheme == "http" ||
|
||||
uri.scheme == "https") {
|
||||
path = uri.toString();
|
||||
} else if (uri.scheme == "file") {
|
||||
path = Util.isWindows() ? uri.path.substring(1) : uri.path;
|
||||
} else {
|
||||
path = (await toFile(uri.toString())).path;
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||
arguments: RedirectArgs(Routes.CREATE,
|
||||
arguments: CreateRouterParams(req: Request(url: path))));
|
||||
}
|
||||
|
||||
String runningAddress() {
|
||||
if (startConfig.value.network == 'unix') {
|
||||
return startConfig.value.address;
|
||||
}
|
||||
return '${startConfig.value.address.split(':').first}:$runningPort';
|
||||
}
|
||||
|
||||
Future<StartConfig> _initDefaultStartConfig() async {
|
||||
if (_defaultStartConfig != null) {
|
||||
return _defaultStartConfig!;
|
||||
}
|
||||
_defaultStartConfig = StartConfig();
|
||||
if (!Util.supportUnixSocket()) {
|
||||
// not support unix socket, use tcp
|
||||
_defaultStartConfig!.network = "tcp";
|
||||
_defaultStartConfig!.address = "127.0.0.1:0";
|
||||
} else {
|
||||
_defaultStartConfig!.network = "unix";
|
||||
_defaultStartConfig!.address =
|
||||
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
|
||||
}
|
||||
_defaultStartConfig!.apiToken = '';
|
||||
return _defaultStartConfig!;
|
||||
}
|
||||
|
||||
Future<StartConfig> loadStartConfig() async {
|
||||
final defaultCfg = await _initDefaultStartConfig();
|
||||
final saveCfg = Database.instance.getStartConfig();
|
||||
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
|
||||
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
|
||||
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
|
||||
return startConfig.value;
|
||||
Future<void> _toCreate(Uri uri) async {
|
||||
final path = (uri.scheme == "magnet" ||
|
||||
uri.scheme == "http" ||
|
||||
uri.scheme == "https")
|
||||
? uri.toString()
|
||||
: (await toFile(uri.toString())).path;
|
||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
|
||||
}
|
||||
|
||||
Future<DownloaderConfig> loadDownloaderConfig() async {
|
||||
try {
|
||||
downloaderConfig.value = await getConfig();
|
||||
downloaderConfig.value = await LibgopeedBoot.instance.getConfig();
|
||||
} catch (e) {
|
||||
logger.w("load downloader config fail", e, StackTrace.current);
|
||||
downloaderConfig.value = DownloaderConfig();
|
||||
@@ -410,7 +344,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
});
|
||||
refreshTrackers();
|
||||
|
||||
await saveConfig();
|
||||
await LibgopeedBoot.instance.putConfig(downloaderConfig.value);
|
||||
}
|
||||
|
||||
refreshTrackers() {
|
||||
@@ -439,7 +373,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<List<String>> _fetchTrackers(String subscribeUrl) async {
|
||||
final resp = await proxyRequest(subscribeUrl);
|
||||
final resp = await LibgopeedBoot.instance.proxyRequest(subscribeUrl);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to get trackers, status code: ${resp.statusCode}');
|
||||
@@ -496,12 +430,4 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
args: ['--${Args.flagHidden}']);
|
||||
autoStartup.value = await launchAtStartup.isEnabled();
|
||||
}
|
||||
|
||||
Future<void> saveConfig() async {
|
||||
Database.instance.saveStartConfig(StartConfigEntity(
|
||||
network: startConfig.value.network,
|
||||
address: startConfig.value.address,
|
||||
apiToken: startConfig.value.apiToken));
|
||||
await putConfig(downloaderConfig.value);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import '../../app/controllers/app_controller.dart';
|
||||
|
||||
class CreateController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
// final files = [].obs;
|
||||
final RxList fileInfos = [].obs;
|
||||
final RxList openedFolders = [].obs;
|
||||
final selectedIndexes = <int>[].obs;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
|
||||
part 'create_router_params.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class CreateRouterParams {
|
||||
Request? req;
|
||||
Options? opt;
|
||||
|
||||
CreateRouterParams({
|
||||
this.req,
|
||||
this.opt,
|
||||
});
|
||||
|
||||
factory CreateRouterParams.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
_$CreateRouterParamsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$CreateRouterParamsToJson(this);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'create_router_params.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CreateRouterParams _$CreateRouterParamsFromJson(Map<String, dynamic> json) =>
|
||||
CreateRouterParams(
|
||||
req: json['req'] == null
|
||||
? null
|
||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
opt: json['opt'] == null
|
||||
? null
|
||||
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateRouterParamsToJson(CreateRouterParams instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('req', instance.req?.toJson());
|
||||
writeNotNull('opt', instance.opt?.toJson());
|
||||
return val;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:contentsize_tabbarview/contentsize_tabbarview.dart';
|
||||
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -9,12 +7,11 @@ import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../api/model/resolve_result.dart';
|
||||
import '../../../../api/model/task.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/message.dart';
|
||||
@@ -22,11 +19,10 @@ import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../../views/compact_checkbox.dart';
|
||||
import '../../../views/directory_selector.dart';
|
||||
import '../../../views/file_tree_view.dart';
|
||||
import '../../../views/file_list_view.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
import '../../history/views/history_view.dart';
|
||||
import '../controllers/create_controller.dart';
|
||||
import '../dto/create_router_params.dart';
|
||||
|
||||
class CreateView extends GetView<CreateController> {
|
||||
final _confirmFormKey = GlobalKey<FormState>();
|
||||
@@ -40,20 +36,9 @@ class CreateView extends GetView<CreateController> {
|
||||
final _proxyPortController = TextEditingController();
|
||||
final _proxyUsrController = TextEditingController();
|
||||
final _proxyPwdController = TextEditingController();
|
||||
final _httpHeaderControllers = [
|
||||
(
|
||||
name: TextEditingController(text: "User-Agent"),
|
||||
value: TextEditingController()
|
||||
),
|
||||
(
|
||||
name: TextEditingController(text: "Cookie"),
|
||||
value: TextEditingController()
|
||||
),
|
||||
(
|
||||
name: TextEditingController(text: "Referer"),
|
||||
value: TextEditingController()
|
||||
),
|
||||
];
|
||||
final _httpUaController = TextEditingController();
|
||||
final _httpCookieController = TextEditingController();
|
||||
final _httpRefererController = TextEditingController();
|
||||
final _btTrackerController = TextEditingController();
|
||||
|
||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||
@@ -75,91 +60,32 @@ class CreateView extends GetView<CreateController> {
|
||||
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
||||
}
|
||||
|
||||
final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
|
||||
if (routerParams?.req?.url.isNotEmpty ?? false) {
|
||||
// get url from route arguments
|
||||
final url = routerParams!.req!.url;
|
||||
_urlController.text = url;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
final uppercaseUrl = url.toUpperCase();
|
||||
Protocol? protocol;
|
||||
if (uppercaseUrl.startsWith("HTTP:") ||
|
||||
uppercaseUrl.startsWith("HTTPS:")) {
|
||||
protocol = Protocol.http;
|
||||
}
|
||||
if (uppercaseUrl.startsWith("MAGNET:") ||
|
||||
uppercaseUrl.endsWith(".TORRENT")) {
|
||||
protocol = Protocol.bt;
|
||||
}
|
||||
if (protocol != null) {
|
||||
final extraHandlers = {
|
||||
Protocol.http: () {
|
||||
final reqExtra = ReqExtraHttp.fromJson(
|
||||
jsonDecode(jsonEncode(routerParams.req!.extra)));
|
||||
_httpHeaderControllers.clear();
|
||||
reqExtra.header.forEach((key, value) {
|
||||
_httpHeaderControllers.add(
|
||||
(
|
||||
name: TextEditingController(text: key),
|
||||
value: TextEditingController(text: value),
|
||||
),
|
||||
);
|
||||
});
|
||||
_skipVerifyCertController.value = routerParams.req!.skipVerifyCert;
|
||||
},
|
||||
Protocol.bt: () {
|
||||
final reqExtra = ReqExtraBt.fromJson(
|
||||
jsonDecode(jsonEncode(routerParams.req!.extra)));
|
||||
_btTrackerController.text = reqExtra.trackers.join("\n");
|
||||
},
|
||||
};
|
||||
if (routerParams.req?.extra != null) {
|
||||
extraHandlers[protocol]?.call();
|
||||
}
|
||||
final String? filePath = Get.rootDelegate.arguments();
|
||||
if (_urlController.text.isEmpty) {
|
||||
if (filePath?.isNotEmpty ?? false) {
|
||||
// get file path from route arguments
|
||||
_urlController.text = filePath!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
} else {
|
||||
// read clipboard
|
||||
Clipboard.getData('text/plain').then((value) {
|
||||
if (value?.text?.isNotEmpty ?? false) {
|
||||
if (_availableSchemes
|
||||
.where((e) =>
|
||||
value!.text!.startsWith(e) ||
|
||||
value.text!.startsWith(e.toUpperCase()))
|
||||
.isNotEmpty) {
|
||||
_urlController.text = value!.text!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
return;
|
||||
}
|
||||
|
||||
// handle options
|
||||
if (routerParams.opt != null) {
|
||||
_renameController.text = routerParams.opt!.name;
|
||||
_pathController.text = routerParams.opt!.path;
|
||||
|
||||
final optionsHandlers = {
|
||||
Protocol.http: () {
|
||||
final opt = routerParams.opt!;
|
||||
_renameController.text = opt.name;
|
||||
_pathController.text = opt.path;
|
||||
if (opt.extra != null) {
|
||||
final optsExtraHttp =
|
||||
OptsExtraHttp.fromJson(jsonDecode(jsonEncode(opt.extra)));
|
||||
_connectionsController.text =
|
||||
optsExtraHttp.connections.toString();
|
||||
}
|
||||
},
|
||||
Protocol.bt: null,
|
||||
};
|
||||
if (routerParams.opt?.extra != null) {
|
||||
optionsHandlers[protocol]?.call();
|
||||
recognizeMagnetUri(value!.text!);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (_urlController.text.isEmpty) {
|
||||
// read clipboard
|
||||
Clipboard.getData('text/plain').then((value) {
|
||||
if (value?.text?.isNotEmpty ?? false) {
|
||||
if (_availableSchemes
|
||||
.where((e) =>
|
||||
value!.text!.startsWith(e) ||
|
||||
value.text!.startsWith(e.toUpperCase()))
|
||||
.isNotEmpty) {
|
||||
_urlController.text = value!.text!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
return;
|
||||
}
|
||||
|
||||
recognizeMagnetUri(value!.text!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
@@ -510,105 +436,56 @@ class CreateView extends GetView<CreateController> {
|
||||
)
|
||||
],
|
||||
),
|
||||
DefaultTabController(
|
||||
length: 2,
|
||||
child: ContentSizeTabBarView(
|
||||
controller:
|
||||
controller.advancedTabController,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
..._httpHeaderControllers.map((e) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: e.name,
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
'httpHeaderName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 10)),
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: e.value,
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
'httpHeaderValue'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 10)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
_httpHeaderControllers.add(
|
||||
(
|
||||
name:
|
||||
TextEditingController(),
|
||||
value:
|
||||
TextEditingController(),
|
||||
),
|
||||
);
|
||||
controller.showAdvanced
|
||||
.update((val) => val);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon:
|
||||
const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
if (_httpHeaderControllers
|
||||
.length <=
|
||||
1) {
|
||||
return;
|
||||
}
|
||||
_httpHeaderControllers
|
||||
.remove(e);
|
||||
controller.showAdvanced
|
||||
.update((val) => val);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 10),
|
||||
child: CompactCheckbox(
|
||||
label: 'skipVerifyCert'.tr,
|
||||
value: _skipVerifyCertController
|
||||
.value,
|
||||
onChanged: (bool? value) {
|
||||
_skipVerifyCertController
|
||||
.value = value ?? false;
|
||||
},
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
AutoScaleTabBarView(
|
||||
controller: controller.advancedTabController,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _httpUaController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'User-Agent',
|
||||
)),
|
||||
TextFormField(
|
||||
controller: _httpCookieController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Cookie',
|
||||
)),
|
||||
TextFormField(
|
||||
controller: _httpRefererController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Referer',
|
||||
)),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 10),
|
||||
child: CompactCheckbox(
|
||||
label: 'skipVerifyCert'.tr,
|
||||
value:
|
||||
_skipVerifyCertController.value,
|
||||
onChanged: (bool? value) {
|
||||
_skipVerifyCertController.value =
|
||||
value ?? false;
|
||||
},
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _btTrackerController,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Trackers',
|
||||
hintText: 'addTrackerHit'.tr,
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _btTrackerController,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Trackers',
|
||||
hintText: 'addTrackerHit'.tr,
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
).paddingOnly(top: 16),
|
||||
@@ -717,7 +594,7 @@ class CreateView extends GetView<CreateController> {
|
||||
final isDirect = controller.directDownload.value || isMultiLine;
|
||||
if (isDirect) {
|
||||
await Future.wait(urls.map((url) {
|
||||
return createTask(CreateTask(
|
||||
return LibgopeedBoot.instance.createTask(CreateTask(
|
||||
req: Request(
|
||||
url: url,
|
||||
extra: parseReqExtra(url),
|
||||
@@ -733,7 +610,7 @@ class CreateView extends GetView<CreateController> {
|
||||
}));
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
} else {
|
||||
final rr = await resolve(Request(
|
||||
final rr = await LibgopeedBoot.instance.resolve(Request(
|
||||
url: submitUrl,
|
||||
extra: parseReqExtra(_urlController.text),
|
||||
proxy: parseProxy(),
|
||||
@@ -767,10 +644,12 @@ class CreateView extends GetView<CreateController> {
|
||||
if (controller.showAdvanced.value) {
|
||||
switch (controller.advancedTabController.index) {
|
||||
case 0:
|
||||
final header = Map<String, String>.fromEntries(_httpHeaderControllers
|
||||
.map((e) => MapEntry(e.name.text, e.value.text)));
|
||||
header.removeWhere(
|
||||
(key, value) => key.trim().isEmpty || value.trim().isEmpty);
|
||||
final header = {
|
||||
"User-Agent": _httpUaController.text,
|
||||
"Cookie": _httpCookieController.text,
|
||||
"Referer": _httpRefererController.text,
|
||||
};
|
||||
header.removeWhere((key, value) => value.trim().isEmpty);
|
||||
if (header.isNotEmpty) {
|
||||
reqExtra = ReqExtraHttp()..header = header;
|
||||
}
|
||||
@@ -819,13 +698,7 @@ class CreateView extends GetView<CreateController> {
|
||||
child: Form(
|
||||
key: createFormKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: FileTreeView(
|
||||
files: rr.res.files,
|
||||
initialValues: rr.res.files.asMap().keys.toList(),
|
||||
onSelectionChanged: (List<int> values) {
|
||||
controller.selectedIndexes.value = values;
|
||||
},
|
||||
)),
|
||||
child: FileListView(files: rr.res.files)),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -868,23 +741,28 @@ class CreateView extends GetView<CreateController> {
|
||||
await Future.wait(
|
||||
controller.selectedIndexes.map((index) {
|
||||
final file = rr.res.files[index];
|
||||
return createTask(CreateTask(
|
||||
req: file.req!..proxy = parseProxy(),
|
||||
opt: Options(
|
||||
name: file.name,
|
||||
path: path.join(_pathController.text,
|
||||
rr.res.name, file.path),
|
||||
selectFiles: [],
|
||||
extra: optExtra)));
|
||||
return LibgopeedBoot.instance.createTask(
|
||||
CreateTask(
|
||||
req: file.req!..proxy = parseProxy(),
|
||||
opt: Options(
|
||||
name: file.name,
|
||||
path: path.join(
|
||||
_pathController.text,
|
||||
rr.res.name,
|
||||
file.path),
|
||||
selectFiles: [],
|
||||
extra: optExtra)));
|
||||
}));
|
||||
} else {
|
||||
await createTask(CreateTask(
|
||||
rid: rr.id,
|
||||
opt: Options(
|
||||
name: _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles: controller.selectedIndexes,
|
||||
extra: optExtra)));
|
||||
await LibgopeedBoot.instance.createTask(
|
||||
CreateTask(
|
||||
rid: rr.id,
|
||||
opt: Options(
|
||||
name: _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles:
|
||||
controller.selectedIndexes,
|
||||
extra: optExtra)));
|
||||
}
|
||||
Get.back();
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/api.dart';
|
||||
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/extension.dart';
|
||||
|
||||
class ExtensionController extends GetxController {
|
||||
@@ -17,12 +17,13 @@ class ExtensionController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
extensions.value = await getExtensions();
|
||||
extensions.value = await LibgopeedBoot.instance.getExtensions();
|
||||
}
|
||||
|
||||
Future<void> checkUpdate() async {
|
||||
for (final ext in extensions) {
|
||||
final resp = await upgradeCheckExtension(ext.identity);
|
||||
final resp =
|
||||
await LibgopeedBoot.instance.upgradeCheckExtension(ext.identity);
|
||||
if (resp.newVersion.isNotEmpty) {
|
||||
updateFlags[ext.identity] = resp.newVersion;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/entry/libgopeed_boot_web.dart';
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/extension.dart';
|
||||
import '../../../../api/model/install_extension.dart';
|
||||
import '../../../../api/model/switch_extension.dart';
|
||||
@@ -78,8 +79,9 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
}
|
||||
_installBtnController.start();
|
||||
try {
|
||||
await installExtension(InstallExtension(
|
||||
url: _installUrlController.text));
|
||||
await LibgopeedBoot.instance.installExtension(
|
||||
InstallExtension(
|
||||
url: _installUrlController.text));
|
||||
Get.snackbar('tip'.tr, 'extensionInstallSuccess'.tr);
|
||||
await controller.load();
|
||||
} catch (e) {
|
||||
@@ -98,7 +100,7 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
if (dir != null) {
|
||||
MacSecureUtil.saveBookmark(dir);
|
||||
try {
|
||||
await installExtension(
|
||||
await LibgopeedBoot.instance.installExtension(
|
||||
InstallExtension(devMode: true, url: dir));
|
||||
Get.snackbar(
|
||||
'tip'.tr, 'extensionInstallSuccess'.tr);
|
||||
@@ -133,8 +135,10 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
)
|
||||
: Util.isWeb()
|
||||
? Image.network(
|
||||
join(
|
||||
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||
(LibgopeedBoot.instance
|
||||
as LibgopeedBootWeb)
|
||||
.join(
|
||||
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||
width: 48,
|
||||
height: 48,
|
||||
)
|
||||
@@ -155,9 +159,9 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
value: !extension.disabled,
|
||||
onChanged: (value) async {
|
||||
try {
|
||||
await switchExtension(
|
||||
extension.identity,
|
||||
SwitchExtension(status: value));
|
||||
await LibgopeedBoot.instance
|
||||
.switchExtension(extension.identity,
|
||||
SwitchExtension(status: value));
|
||||
await controller.load();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
@@ -354,10 +358,11 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
try {
|
||||
confrimController.start();
|
||||
if (formKey.currentState?.saveAndValidate() == true) {
|
||||
await updateExtensionSettings(
|
||||
extension.identity,
|
||||
UpdateExtensionSettings(
|
||||
settings: formKey.currentState!.value));
|
||||
await LibgopeedBoot.instance
|
||||
.updateExtensionSettings(
|
||||
extension.identity,
|
||||
UpdateExtensionSettings(
|
||||
settings: formKey.currentState!.value));
|
||||
await controller.load();
|
||||
Get.back();
|
||||
}
|
||||
@@ -454,7 +459,8 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await deleteExtension(extension.identity);
|
||||
await LibgopeedBoot.instance
|
||||
.deleteExtension(extension.identity);
|
||||
await controller.load();
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
@@ -487,18 +493,19 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
onPressed: () async {
|
||||
confrimController.start();
|
||||
try {
|
||||
await updateExtension(extension.identity);
|
||||
await LibgopeedBoot.instance
|
||||
.upgradeExtension(extension.identity);
|
||||
await controller.load();
|
||||
controller.updateFlags.remove(extension.identity);
|
||||
Get.back();
|
||||
showMessage('tip'.tr, 'extensionUpdateSuccess'.tr);
|
||||
showMessage('tip'.tr, 'extensionUpgradeSuccess'.tr);
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
confrimController.stop();
|
||||
}
|
||||
},
|
||||
child: Text('newVersionUpdate'.tr),
|
||||
child: Text('newVersionUpgrade'.tr),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/redirect_controller.dart';
|
||||
|
||||
class RedirectBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<RedirectController>(
|
||||
() => RedirectController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class RedirectController extends GetxController {}
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/redirect_controller.dart';
|
||||
|
||||
class RedirectArgs {
|
||||
final String page;
|
||||
final dynamic arguments;
|
||||
|
||||
RedirectArgs(this.page, {this.arguments});
|
||||
}
|
||||
|
||||
class RedirectView extends GetView<RedirectController> {
|
||||
const RedirectView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final redirectArgs = Get.rootDelegate.arguments() as RedirectArgs;
|
||||
// Waiting for previous page controller to delete, avoid deleting controller that route page after redirect
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 350));
|
||||
Get.rootDelegate
|
||||
.offAndToNamed(redirectArgs.page, arguments: redirectArgs.arguments);
|
||||
});
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/api.dart';
|
||||
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
|
||||
class SettingController extends GetxController {
|
||||
final tapStatues = <String, bool>{}.obs;
|
||||
@@ -28,12 +29,13 @@ class SettingController extends GetxController {
|
||||
void fetchLatestVersion() async {
|
||||
String? releaseDataStr;
|
||||
try {
|
||||
releaseDataStr = (await proxyRequest(
|
||||
releaseDataStr = (await LibgopeedBoot.instance.proxyRequest(
|
||||
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
||||
.data;
|
||||
} catch (e) {
|
||||
releaseDataStr =
|
||||
(await proxyRequest("https://gopeed.com/api/release")).data;
|
||||
releaseDataStr = (await LibgopeedBoot.instance
|
||||
.proxyRequest("https://gopeed.com/api/release"))
|
||||
.data;
|
||||
}
|
||||
if (releaseDataStr == null) {
|
||||
return;
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
@@ -17,7 +18,6 @@ import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/log_util.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
import '../../../../util/scheme_register/scheme_register.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/check_list_view.dart';
|
||||
import '../../../views/directory_selector.dart';
|
||||
@@ -36,7 +36,6 @@ class SettingView extends GetView<SettingController> {
|
||||
Widget build(BuildContext context) {
|
||||
final appController = Get.find<AppController>();
|
||||
final downloaderCfg = appController.downloaderConfig;
|
||||
final startCfg = appController.startConfig;
|
||||
|
||||
Timer? timer;
|
||||
Future<bool> debounceSave(
|
||||
@@ -52,8 +51,8 @@ class SettingView extends GetView<SettingController> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
appController
|
||||
.saveConfig()
|
||||
LibgopeedBoot.instance
|
||||
.putConfig(downloaderCfg.value)
|
||||
.then((_) => completer.complete(true))
|
||||
.onError(completer.completeError);
|
||||
if (needRestart) {
|
||||
@@ -112,7 +111,7 @@ class SettingView extends GetView<SettingController> {
|
||||
});
|
||||
|
||||
final buildDefaultDirectDownload =
|
||||
_buildConfigItem('defaultDirectDownload', () {
|
||||
_buildConfigItem('defaultDirectDownload'.tr, () {
|
||||
return appController.downloaderConfig.value.extra.defaultDirectDownload
|
||||
? 'on'.tr
|
||||
: 'off'.tr;
|
||||
@@ -162,7 +161,7 @@ class SettingView extends GetView<SettingController> {
|
||||
// Currently auto startup only support Windows and Linux
|
||||
final buildAutoStartup = !Util.isWindows() && !Util.isLinux()
|
||||
? () => null
|
||||
: _buildConfigItem('launchAtStartup', () {
|
||||
: _buildConfigItem('launchAtStartup'.tr, () {
|
||||
return appController.autoStartup.value ? 'on'.tr : 'off'.tr;
|
||||
}, (Key key) {
|
||||
return Container(
|
||||
@@ -230,9 +229,8 @@ class SettingView extends GetView<SettingController> {
|
||||
],
|
||||
);
|
||||
});
|
||||
final buildHttpUseServerCtime = _buildConfigItem(
|
||||
'useServerCtime', () => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr,
|
||||
(Key key) {
|
||||
final buildHttpUseServerCtime = _buildConfigItem('useServerCtime'.tr,
|
||||
() => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr, (Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
@@ -422,37 +420,6 @@ class SettingView extends GetView<SettingController> {
|
||||
].where((e) => e != null).map((e) => e!).toList(),
|
||||
);
|
||||
});
|
||||
final buildBtDefaultClientConfig = !Util.isWindows()
|
||||
? () => null
|
||||
: _buildConfigItem('setAsDefaultBtClient', () {
|
||||
return appController.downloaderConfig.value.extra.defaultBtClient
|
||||
? 'on'.tr
|
||||
: 'off'.tr;
|
||||
}, (Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value:
|
||||
appController.downloaderConfig.value.extra.defaultBtClient,
|
||||
onChanged: (bool value) async {
|
||||
try {
|
||||
if (value) {
|
||||
registerDefaultTorrentClient();
|
||||
} else {
|
||||
unregisterDefaultTorrentClient();
|
||||
}
|
||||
appController.downloaderConfig.update((val) {
|
||||
val!.extra.defaultBtClient = value;
|
||||
});
|
||||
await debounceSave();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
logger.e('register default torrent client fail', e);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// ui config items start
|
||||
final buildTheme = _buildConfigItem(
|
||||
@@ -563,7 +530,7 @@ class SettingView extends GetView<SettingController> {
|
||||
launchUrl(Uri.parse('https://gopeed.com'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text('newVersionUpdate'.tr),
|
||||
child: Text('newVersionUpgrade'.tr),
|
||||
),
|
||||
],
|
||||
));
|
||||
@@ -670,7 +637,7 @@ class SettingView extends GetView<SettingController> {
|
||||
final portController = TextEditingController(text: port);
|
||||
updateAddress() async {
|
||||
final newAddress = '${ipController.text}:${portController.text}';
|
||||
if (newAddress != startCfg.value.address) {
|
||||
if (newAddress != proxy.host) {
|
||||
proxy.host = newAddress;
|
||||
|
||||
await debounceSave();
|
||||
@@ -751,152 +718,152 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
|
||||
// advanced config API items start
|
||||
final buildApiProtocol = _buildConfigItem(
|
||||
'protocol',
|
||||
() => startCfg.value.network == 'tcp'
|
||||
? 'TCP ${startCfg.value.address}'
|
||||
: 'Unix',
|
||||
(Key key) {
|
||||
final items = <Widget>[
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: startCfg.value.network,
|
||||
onChanged: Util.isDesktop()
|
||||
? (value) async {
|
||||
startCfg.update((val) {
|
||||
val!.network = value!;
|
||||
});
|
||||
// final buildApiProtocol = _buildConfigItem(
|
||||
// 'protocol',
|
||||
// () => startCfg.value.host == 'tcp'
|
||||
// ? 'TCP ${startCfg.value.address}'
|
||||
// : 'Unix',
|
||||
// (Key key) {
|
||||
// final items = <Widget>[
|
||||
// SizedBox(
|
||||
// width: 80,
|
||||
// child: DropdownButtonFormField<String>(
|
||||
// value: startCfg.value.host,
|
||||
// onChanged: Util.isDesktop()
|
||||
// ? (value) async {
|
||||
// startCfg.update((val) {
|
||||
// val!.host = value!;
|
||||
// });
|
||||
|
||||
await debounceSave(needRestart: true);
|
||||
}
|
||||
: null,
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'tcp',
|
||||
child: Text('TCP'),
|
||||
),
|
||||
Util.supportUnixSocket()
|
||||
? const DropdownMenuItem<String>(
|
||||
value: 'unix',
|
||||
child: Text('Unix'),
|
||||
)
|
||||
: null,
|
||||
].where((e) => e != null).map((e) => e!).toList(),
|
||||
),
|
||||
)
|
||||
];
|
||||
if (Util.isDesktop() && startCfg.value.network == 'tcp') {
|
||||
final arr = startCfg.value.address.split(':');
|
||||
var ip = '127.0.0.1';
|
||||
var port = '0';
|
||||
if (arr.length > 1) {
|
||||
ip = arr[0];
|
||||
port = arr[1];
|
||||
}
|
||||
// await debounceSave(needRestart: true);
|
||||
// }
|
||||
// : null,
|
||||
// items: [
|
||||
// const DropdownMenuItem<String>(
|
||||
// value: 'tcp',
|
||||
// child: Text('TCP'),
|
||||
// ),
|
||||
// Util.supportUnixSocket()
|
||||
// ? const DropdownMenuItem<String>(
|
||||
// value: 'unix',
|
||||
// child: Text('Unix'),
|
||||
// )
|
||||
// : null,
|
||||
// ].where((e) => e != null).map((e) => e!).toList(),
|
||||
// ),
|
||||
// )
|
||||
// ];
|
||||
// if (Util.isDesktop() && startCfg.value.host == 'tcp') {
|
||||
// final arr = startCfg.value.address.split(':');
|
||||
// var ip = '127.0.0.1';
|
||||
// var port = '0';
|
||||
// if (arr.length > 1) {
|
||||
// ip = arr[0];
|
||||
// port = arr[1];
|
||||
// }
|
||||
|
||||
final ipController = TextEditingController(text: ip);
|
||||
final portController = TextEditingController(text: port);
|
||||
updateAddress() async {
|
||||
if (ipController.text.isEmpty || portController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final newAddress = '${ipController.text}:${portController.text}';
|
||||
if (newAddress != startCfg.value.address) {
|
||||
startCfg.value.address = newAddress;
|
||||
// final ipController = TextEditingController(text: ip);
|
||||
// final portController = TextEditingController(text: port);
|
||||
// updateAddress() async {
|
||||
// if (ipController.text.isEmpty || portController.text.isEmpty) {
|
||||
// return;
|
||||
// }
|
||||
// final newAddress = '${ipController.text}:${portController.text}';
|
||||
// if (newAddress != startCfg.value.address) {
|
||||
// startCfg.value.address = newAddress;
|
||||
|
||||
final saved = await debounceSave(
|
||||
check: () async {
|
||||
// Check if address already in use
|
||||
final configIp = ipController.text;
|
||||
final configPort = int.parse(portController.text);
|
||||
if (configPort == 0) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
final socket = await Socket.connect(configIp, configPort,
|
||||
timeout: const Duration(seconds: 3));
|
||||
socket.close();
|
||||
return 'portInUse'
|
||||
.trParams({'port': configPort.toString()});
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
needRestart: true);
|
||||
// final saved = await debounceSave(
|
||||
// check: () async {
|
||||
// // Check if address already in use
|
||||
// final configIp = ipController.text;
|
||||
// final configPort = int.parse(portController.text);
|
||||
// if (configPort == 0) {
|
||||
// return '';
|
||||
// }
|
||||
// try {
|
||||
// final socket = await Socket.connect(configIp, configPort,
|
||||
// timeout: const Duration(seconds: 3));
|
||||
// socket.close();
|
||||
// return 'portInUse'
|
||||
// .trParams({'port': configPort.toString()});
|
||||
// } catch (e) {
|
||||
// return '';
|
||||
// }
|
||||
// },
|
||||
// needRestart: true);
|
||||
|
||||
// If save failed, restore the old address
|
||||
if (!saved) {
|
||||
final oldAddress =
|
||||
(await appController.loadStartConfig()).address;
|
||||
startCfg.update((val) async {
|
||||
val!.address = oldAddress;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// // If save failed, restore the old address
|
||||
// if (!saved) {
|
||||
// // final oldAddress =
|
||||
// // (await appController.loadStartConfig()).address;
|
||||
// // startCfg.update((val) async {
|
||||
// // val!.address = oldAddress;
|
||||
// // });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
ipController.addListener(updateAddress);
|
||||
portController.addListener(updateAddress);
|
||||
items.addAll([
|
||||
const Padding(padding: EdgeInsets.only(left: 20)),
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: ipController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'IP',
|
||||
contentPadding: EdgeInsets.all(0.0),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 10)),
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: portController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'port'.tr,
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 0, max: 65535),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
// ipController.addListener(updateAddress);
|
||||
// portController.addListener(updateAddress);
|
||||
// items.addAll([
|
||||
// const Padding(padding: EdgeInsets.only(left: 20)),
|
||||
// Flexible(
|
||||
// child: TextFormField(
|
||||
// controller: ipController,
|
||||
// decoration: const InputDecoration(
|
||||
// labelText: 'IP',
|
||||
// contentPadding: EdgeInsets.all(0.0),
|
||||
// ),
|
||||
// keyboardType: TextInputType.number,
|
||||
// inputFormatters: [
|
||||
// FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const Padding(padding: EdgeInsets.only(left: 10)),
|
||||
// Flexible(
|
||||
// child: TextFormField(
|
||||
// controller: portController,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: 'port'.tr,
|
||||
// contentPadding: const EdgeInsets.all(0.0),
|
||||
// ),
|
||||
// keyboardType: TextInputType.number,
|
||||
// inputFormatters: [
|
||||
// FilteringTextInputFormatter.digitsOnly,
|
||||
// NumericalRangeFormatter(min: 0, max: 65535),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ]);
|
||||
// }
|
||||
|
||||
return Form(
|
||||
child: Row(
|
||||
children: items,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
final buildApiToken = _buildConfigItem('apiToken',
|
||||
() => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'set'.tr,
|
||||
(Key key) {
|
||||
final apiTokenController =
|
||||
TextEditingController(text: startCfg.value.apiToken);
|
||||
apiTokenController.addListener(() async {
|
||||
if (apiTokenController.text != startCfg.value.apiToken) {
|
||||
startCfg.value.apiToken = apiTokenController.text;
|
||||
// return Form(
|
||||
// child: Row(
|
||||
// children: items,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// final buildApiToken = _buildConfigItem('apiToken',
|
||||
// () => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'set'.tr,
|
||||
// (Key key) {
|
||||
// final apiTokenController =
|
||||
// TextEditingController(text: startCfg.value.apiToken);
|
||||
// apiTokenController.addListener(() async {
|
||||
// if (apiTokenController.text != startCfg.value.apiToken) {
|
||||
// startCfg.value.apiToken = apiTokenController.text;
|
||||
|
||||
await debounceSave(needRestart: true);
|
||||
}
|
||||
});
|
||||
return TextField(
|
||||
key: key,
|
||||
obscureText: true,
|
||||
controller: apiTokenController,
|
||||
focusNode: FocusNode(),
|
||||
);
|
||||
});
|
||||
// await debounceSave(needRestart: true);
|
||||
// }
|
||||
// });
|
||||
// return TextField(
|
||||
// key: key,
|
||||
// obscureText: true,
|
||||
// controller: apiTokenController,
|
||||
// focusNode: FocusNode(),
|
||||
// );
|
||||
// });
|
||||
|
||||
// advanced config log items start
|
||||
buildLogsDir() {
|
||||
@@ -980,7 +947,6 @@ class SettingView extends GetView<SettingController> {
|
||||
buildBtTrackerSubscribeUrls(),
|
||||
buildBtTrackers(),
|
||||
buildBtSeedConfig(),
|
||||
buildBtDefaultClientConfig(),
|
||||
]),
|
||||
)),
|
||||
Text('ui'.tr),
|
||||
@@ -1027,16 +993,16 @@ class SettingView extends GetView<SettingController> {
|
||||
child: Column(
|
||||
children: _addDivider([buildProxy()]),
|
||||
)),
|
||||
const Text('API'),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildApiProtocol(),
|
||||
Util.isDesktop() && startCfg.value.network == 'tcp'
|
||||
? buildApiToken()
|
||||
: null,
|
||||
]),
|
||||
)),
|
||||
// const Text('API'),
|
||||
// Card(
|
||||
// child: Column(
|
||||
// children: _addDivider([
|
||||
// buildApiProtocol(),
|
||||
// Util.isDesktop() && startCfg.value.host == 'tcp'
|
||||
// ? buildApiToken()
|
||||
// : null,
|
||||
// ]),
|
||||
// )),
|
||||
Text('developer'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/task.dart';
|
||||
|
||||
@@ -8,20 +6,5 @@ class TaskController extends GetxController {
|
||||
final tabIndex = 0.obs;
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final selectTask = Rx<Task?>(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (kIsWeb) {
|
||||
BrowserContextMenu.disableContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
if (kIsWeb) {
|
||||
BrowserContextMenu.enableContextMenu();
|
||||
}
|
||||
}
|
||||
final copyUrlDone = false.obs;
|
||||
}
|
||||
@@ -9,13 +9,5 @@ class TaskDownloadingController extends TaskListController {
|
||||
Status.pause,
|
||||
Status.wait,
|
||||
Status.error
|
||||
], (a, b) {
|
||||
if (a.status == Status.running && b.status != Status.running) {
|
||||
return -1;
|
||||
} else if (a.status != Status.running && b.status == Status.running) {
|
||||
return 1;
|
||||
} else {
|
||||
return b.updatedAt.compareTo(a.updatedAt);
|
||||
}
|
||||
});
|
||||
], (a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/resource.dart';
|
||||
import '../../../../api/model/task.dart';
|
||||
|
||||
@@ -31,7 +31,7 @@ class TaskFilesController extends GetxController {
|
||||
super.onInit();
|
||||
|
||||
final taskId = Get.rootDelegate.parameters['id'];
|
||||
final tasks = await getTasks([]);
|
||||
final tasks = await LibgopeedBoot.instance.getTasks(null);
|
||||
task.value = tasks.firstWhere((element) => element.id == taskId);
|
||||
parseDirMap(task.value!.meta.res!.files);
|
||||
toDir("/");
|
||||
|
||||
@@ -2,8 +2,9 @@ import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/task.dart';
|
||||
import '../../../../api/model/task_filter.dart';
|
||||
|
||||
abstract class TaskListController extends GetxController {
|
||||
List<Status> statuses;
|
||||
@@ -12,7 +13,6 @@ abstract class TaskListController extends GetxController {
|
||||
TaskListController(this.statuses, this.compare);
|
||||
|
||||
final tasks = <Task>[].obs;
|
||||
final selectedTaskIds = <String>[].obs;
|
||||
final isRunning = false.obs;
|
||||
|
||||
late final Timer _timer;
|
||||
@@ -45,7 +45,8 @@ abstract class TaskListController extends GetxController {
|
||||
}
|
||||
|
||||
getTasksState() async {
|
||||
final tasks = await getTasks(statuses);
|
||||
final tasks =
|
||||
await LibgopeedBoot.instance.getTasks(TaskFilter(status: statuses));
|
||||
// sort tasks by create time
|
||||
tasks.sort(compare);
|
||||
this.tasks.value = tasks;
|
||||
|
||||
@@ -9,7 +9,6 @@ class TaskDownloadedView extends GetView<TaskDownloadedController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuildTaskListView(
|
||||
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
|
||||
return BuildTaskListView(tasks: controller.tasks);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ class TaskDownloadingView extends GetView<TaskDownloadingController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BuildTaskListView(
|
||||
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
|
||||
return BuildTaskListView(tasks: controller.tasks);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import 'package:path/path.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/api.dart' as api;
|
||||
import '../../../../api/entry/libgopeed_boot_web.dart';
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../util/browser_download/browser_download.dart';
|
||||
import '../../../../util/file_icon.dart';
|
||||
import '../../../../util/icons.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/breadcrumb_view.dart';
|
||||
import '../../../views/file_icon.dart';
|
||||
import '../controllers/task_files_controller.dart';
|
||||
|
||||
class TaskFilesView extends GetView<TaskFilesController> {
|
||||
@@ -64,8 +66,9 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
[meta.opts.path, meta.res!.name, fileRelativePath]);
|
||||
final fileName = basename(filePath);
|
||||
return ListTile(
|
||||
leading:
|
||||
Icon(fileIcon(fileName, isFolder: file.isDirectory)),
|
||||
leading: file.isDirectory
|
||||
? const Icon(Icons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(fileName)]),
|
||||
title: Text(fileName),
|
||||
subtitle: file.isDirectory
|
||||
? Text('items'.trParams({
|
||||
@@ -82,8 +85,10 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: Util.isWeb()
|
||||
? () {
|
||||
final accessUrl = api.join(
|
||||
"/fs/tasks/${controller.task.value!.id}$fileRelativePath");
|
||||
final accessUrl = (LibgopeedBoot
|
||||
.instance as LibgopeedBootWeb)
|
||||
.join(
|
||||
"/fs/tasks/${controller.task.value!.id}$fileRelativePath");
|
||||
return [
|
||||
IconButton(
|
||||
icon:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/modules/task/views/task_files_view.dart';
|
||||
import 'package:gopeed/app/modules/task/views/task_view.dart';
|
||||
|
||||
import '../modules/create/bindings/create_binding.dart';
|
||||
import '../modules/create/views/create_view.dart';
|
||||
@@ -6,16 +8,12 @@ import '../modules/extension/bindings/extension_binding.dart';
|
||||
import '../modules/extension/views/extension_view.dart';
|
||||
import '../modules/home/bindings/home_binding.dart';
|
||||
import '../modules/home/views/home_view.dart';
|
||||
import '../modules/redirect/bindings/redirect_binding.dart';
|
||||
import '../modules/redirect/views/redirect_view.dart';
|
||||
import '../modules/root/bindings/root_binding.dart';
|
||||
import '../modules/root/views/root_view.dart';
|
||||
import '../modules/setting/bindings/setting_binding.dart';
|
||||
import '../modules/setting/views/setting_view.dart';
|
||||
import '../modules/task/bindings/task_binding.dart';
|
||||
import '../modules/task/bindings/task_files_binding.dart';
|
||||
import '../modules/task/views/task_files_view.dart';
|
||||
import '../modules/task/views/task_view.dart';
|
||||
|
||||
part 'app_routes.dart';
|
||||
|
||||
@@ -70,11 +68,6 @@ class AppPages {
|
||||
page: () => CreateView(),
|
||||
binding: CreateBinding(),
|
||||
),
|
||||
GetPage(
|
||||
name: _Paths.REDIRECT,
|
||||
page: () => const RedirectView(),
|
||||
binding: RedirectBinding(),
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -3,7 +3,6 @@ part of 'app_pages.dart';
|
||||
|
||||
abstract class Routes {
|
||||
Routes._();
|
||||
|
||||
static const ROOT = _Paths.ROOT;
|
||||
static const HOME = _Paths.HOME;
|
||||
static const CREATE = _Paths.CREATE;
|
||||
@@ -11,12 +10,10 @@ abstract class Routes {
|
||||
static const TASK_FILES = TASK + _Paths.TASK_FILES;
|
||||
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
|
||||
static const SETTING = _Paths.HOME + _Paths.SETTING;
|
||||
static const REDIRECT = _Paths.REDIRECT;
|
||||
}
|
||||
|
||||
abstract class _Paths {
|
||||
_Paths._();
|
||||
|
||||
static const ROOT = '/';
|
||||
static const HOME = '/home';
|
||||
static const CREATE = '/create';
|
||||
@@ -24,5 +21,4 @@ abstract class _Paths {
|
||||
static const TASK_FILES = '/files';
|
||||
static const EXTENSION = '/extension';
|
||||
static const SETTING = '/setting';
|
||||
static const REDIRECT = '/redirect';
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
import 'package:contextmenu_plus/contextmenu_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import '../../api/api.dart';
|
||||
import '../../api/libgopeed_boot.dart';
|
||||
import '../../api/model/task.dart';
|
||||
import '../../util/file_icon.dart';
|
||||
import '../../util/icons.dart';
|
||||
import '../../util/message.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../modules/app/controllers/app_controller.dart';
|
||||
import '../modules/task/controllers/task_controller.dart';
|
||||
import '../modules/task/controllers/task_downloaded_controller.dart';
|
||||
import '../modules/task/controllers/task_downloading_controller.dart';
|
||||
import '../modules/task/views/task_view.dart';
|
||||
import '../routes/app_pages.dart';
|
||||
import 'file_icon.dart';
|
||||
|
||||
class BuildTaskListView extends GetView {
|
||||
final List<Task> tasks;
|
||||
final List<String> selectedTaskIds;
|
||||
|
||||
const BuildTaskListView({
|
||||
Key? key,
|
||||
required this.tasks,
|
||||
required this.selectedTaskIds,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -61,15 +57,11 @@ class BuildTaskListView extends GetView {
|
||||
return task.status == Status.running;
|
||||
}
|
||||
|
||||
bool isSelect() {
|
||||
return selectedTaskIds.contains(task.id);
|
||||
}
|
||||
|
||||
bool isFolderTask() {
|
||||
return task.isFolder;
|
||||
}
|
||||
|
||||
Future<void> showDeleteDialog(List<String> ids) {
|
||||
Future<void> showDeleteDialog(String id) {
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
final context = Get.context!;
|
||||
@@ -78,8 +70,7 @@ class BuildTaskListView extends GetView {
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(
|
||||
'deleteTask'.trParams({'count': ids.length.toString()})),
|
||||
title: Text('deleteTask'.tr),
|
||||
content: Obx(() => CheckboxListTile(
|
||||
value: appController
|
||||
.downloaderConfig.value.extra.lastDeleteTaskKeep,
|
||||
@@ -104,8 +95,9 @@ class BuildTaskListView extends GetView {
|
||||
try {
|
||||
final force = !appController
|
||||
.downloaderConfig.value.extra.lastDeleteTaskKeep;
|
||||
await appController.saveConfig();
|
||||
await deleteTasks(ids, force);
|
||||
LibgopeedBoot.instance
|
||||
.putConfig(appController.downloaderConfig.value);
|
||||
await LibgopeedBoot.instance.deleteTask(id, force);
|
||||
Get.back();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
@@ -131,7 +123,7 @@ class BuildTaskListView extends GetView {
|
||||
icon: const Icon(Icons.pause),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await pauseTask(task.id);
|
||||
await LibgopeedBoot.instance.pauseTask(task.id);
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
}
|
||||
@@ -142,7 +134,7 @@ class BuildTaskListView extends GetView {
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: () async {
|
||||
try {
|
||||
await continueTask(task.id);
|
||||
await LibgopeedBoot.instance.continueTask(task.id);
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
}
|
||||
@@ -153,35 +145,12 @@ class BuildTaskListView extends GetView {
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
showDeleteDialog([task.id]);
|
||||
showDeleteDialog(task.id);
|
||||
},
|
||||
));
|
||||
return list;
|
||||
}
|
||||
|
||||
Widget buildContextItem(IconData icon, String label, Function() onTap,
|
||||
{bool enabled = true}) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
visualDensity: const VisualDensity(vertical: -1),
|
||||
minLeadingWidth: 12,
|
||||
leading: Icon(icon, size: 18),
|
||||
title: Text(label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, // Make the text bold
|
||||
)),
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
try {
|
||||
await onTap();
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
}
|
||||
},
|
||||
enabled: enabled,
|
||||
);
|
||||
}
|
||||
|
||||
double getProgress() {
|
||||
final totalSize = task.meta.res?.size ?? 0;
|
||||
return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize;
|
||||
@@ -200,127 +169,53 @@ class BuildTaskListView extends GetView {
|
||||
}
|
||||
|
||||
final taskController = Get.find<TaskController>();
|
||||
final taskListController = taskController.tabIndex.value == 0
|
||||
? Get.find<TaskDownloadingController>()
|
||||
: Get.find<TaskDownloadedController>();
|
||||
|
||||
// Filter selected task ids that are still in the task list
|
||||
filterSelectedTaskIds(Iterable<String> selectedTaskIds) => selectedTaskIds
|
||||
.where((id) => tasks.any((task) => task.id == id))
|
||||
.toList();
|
||||
|
||||
return ContextMenuArea(
|
||||
width: 140,
|
||||
builder: (context) => [
|
||||
buildContextItem(Icons.checklist, 'selectAll'.tr, () {
|
||||
if (tasks.isEmpty) return;
|
||||
|
||||
if (selectedTaskIds.isNotEmpty) {
|
||||
taskListController.selectedTaskIds([]);
|
||||
} else {
|
||||
taskListController.selectedTaskIds(tasks.map((e) => e.id).toList());
|
||||
}
|
||||
}),
|
||||
buildContextItem(Icons.check, 'select'.tr, () {
|
||||
if (isSelect()) {
|
||||
taskListController.selectedTaskIds(taskListController
|
||||
.selectedTaskIds
|
||||
.where((element) => element != task.id)
|
||||
.toList());
|
||||
} else {
|
||||
taskListController.selectedTaskIds(
|
||||
[...taskListController.selectedTaskIds, task.id]);
|
||||
}
|
||||
}),
|
||||
const Divider(
|
||||
indent: 8,
|
||||
endIndent: 8,
|
||||
),
|
||||
buildContextItem(Icons.play_arrow, 'continue'.tr, () async {
|
||||
try {
|
||||
await continueAllTasks(filterSelectedTaskIds(
|
||||
{...taskListController.selectedTaskIds, task.id}));
|
||||
} finally {
|
||||
taskListController.selectedTaskIds([]);
|
||||
}
|
||||
}, enabled: !isDone() && !isRunning()),
|
||||
buildContextItem(Icons.pause, 'pause'.tr, () async {
|
||||
try {
|
||||
await pauseAllTasks(filterSelectedTaskIds(
|
||||
{...taskListController.selectedTaskIds, task.id}));
|
||||
} finally {
|
||||
taskListController.selectedTaskIds([]);
|
||||
}
|
||||
}, enabled: !isDone() && isRunning()),
|
||||
buildContextItem(Icons.delete, 'delete'.tr, () async {
|
||||
try {
|
||||
await showDeleteDialog(filterSelectedTaskIds(
|
||||
{...taskListController.selectedTaskIds, task.id}));
|
||||
} finally {
|
||||
taskListController.selectedTaskIds([]);
|
||||
}
|
||||
}),
|
||||
],
|
||||
child: Obx(
|
||||
() => Card(
|
||||
elevation: 4.0,
|
||||
shape: isSelect()
|
||||
? RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
taskController.scaffoldKey.currentState?.openEndDrawer();
|
||||
taskController.selectTask.value = task;
|
||||
},
|
||||
onDoubleTap: () {
|
||||
task.open();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return Card(
|
||||
elevation: 4.0,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
taskController.scaffoldKey.currentState?.openEndDrawer();
|
||||
taskController.selectTask.value = task;
|
||||
},
|
||||
onDoubleTap: () {
|
||||
task.open();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(task.name),
|
||||
leading: isFolderTask()
|
||||
? const Icon(FaIcons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(task.name)])),
|
||||
Row(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(task.name),
|
||||
leading: Icon(
|
||||
fileIcon(task.name,
|
||||
isFolder: isFolderTask(),
|
||||
isBitTorrent: task.protocol == Protocol.bt),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
getProgressText(),
|
||||
style: Get.textTheme.bodyLarge
|
||||
?.copyWith(color: Get.theme.disabledColor),
|
||||
).padding(left: 18)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text("${Util.fmtByte(task.progress.speed)} / s",
|
||||
style: Get.textTheme.titleSmall),
|
||||
...buildActions()
|
||||
],
|
||||
)),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
getProgressText(),
|
||||
style: Get.textTheme.bodyLarge
|
||||
?.copyWith(color: Get.theme.disabledColor),
|
||||
).padding(left: 18)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text("${Util.fmtByte(task.progress.speed)} / s",
|
||||
style: Get.textTheme.titleSmall),
|
||||
...buildActions()
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
isDone()
|
||||
? Container()
|
||||
: LinearProgressIndicator(
|
||||
value: getProgress(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)).padding(horizontal: 14, top: 8),
|
||||
),
|
||||
);
|
||||
isDone()
|
||||
? Container()
|
||||
: LinearProgressIndicator(
|
||||
value: getProgress(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)).padding(horizontal: 14, top: 8);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../icon/gopeed_icons.dart';
|
||||
|
||||
final Map<IconData, List<String>> iconConfigMap = {
|
||||
Gopeed.install: ['exe', 'msi', 'dmg', 'deb', 'rpm'],
|
||||
Gopeed.android: ['apk'],
|
||||
Gopeed.app_store_ios: ['ipa'],
|
||||
Gopeed.file_bt: ['torrent'],
|
||||
Gopeed.cd: ['iso'],
|
||||
Gopeed.html5: ['html', 'htm'],
|
||||
Gopeed.file_alt: ['txt', 'md', 'log', 'csv', 'tsv', 'json', 'yaml', 'yml'],
|
||||
Gopeed.file_pdf: ['pdf'],
|
||||
Gopeed.file_word: ['doc', 'docx'],
|
||||
Gopeed.file_excel: ['xls', 'xlsx'],
|
||||
Gopeed.file_powerpoint: ['ppt', 'pptx'],
|
||||
Gopeed.file_archive: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'],
|
||||
Gopeed.file_image: [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'bmp',
|
||||
'tiff',
|
||||
'svg',
|
||||
'webp'
|
||||
],
|
||||
Gopeed.file_audio: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a'],
|
||||
Gopeed.file_video: ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm'],
|
||||
Gopeed.file_code: [
|
||||
'js',
|
||||
'css',
|
||||
'json',
|
||||
'xml',
|
||||
'java',
|
||||
'cpp',
|
||||
'dart',
|
||||
'py',
|
||||
'rb',
|
||||
'php',
|
||||
'ts',
|
||||
'swift',
|
||||
'go',
|
||||
'rs'
|
||||
],
|
||||
Gopeed.file: [''],
|
||||
};
|
||||
|
||||
final Map<String, IconData> _iconCache = Map.fromEntries(
|
||||
iconConfigMap.entries.expand(
|
||||
(entry) => entry.value.map((ext) => MapEntry(ext, entry.key)),
|
||||
),
|
||||
);
|
||||
|
||||
String fileExt(String? name) {
|
||||
if (name == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
final ext = name.split('.').last;
|
||||
if (ext.length > 8) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ext.toLowerCase();
|
||||
}
|
||||
|
||||
IconData fileIcon(String? name,
|
||||
{bool isFolder = false, bool isBitTorrent = false}) {
|
||||
if (isFolder) {
|
||||
return isBitTorrent ? Gopeed.folder_bt : Gopeed.folder;
|
||||
}
|
||||
|
||||
final ext = fileExt(name);
|
||||
return _iconCache[ext] ?? Gopeed.file;
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluent;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../api/model/resource.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../../util/file_icon.dart';
|
||||
import '../../util/icons.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../modules/app/controllers/app_controller.dart';
|
||||
import '../modules/create/controllers/create_controller.dart';
|
||||
|
||||
class FileListView extends GetView {
|
||||
final List<FileInfo> files;
|
||||
|
||||
FileListView({
|
||||
Key? key,
|
||||
required this.files,
|
||||
}) : super(key: key);
|
||||
final parentController = Get.find<CreateController>();
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
List findChildFileIdsR(List children) {
|
||||
List res = [];
|
||||
for (var k in children) {
|
||||
if (parentController.fileInfos[k]['type'] == 'folder') {
|
||||
res.addAll(
|
||||
findChildFileIdsR(parentController.fileInfos[k]['children']));
|
||||
} else {
|
||||
res.add(parentController.fileInfos[k]['fileId']);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
void clearDescendantFolderSizeR(int id) {
|
||||
if (parentController.fileInfos[id]['type'] == 'folder') {
|
||||
parentController.fileInfos[id]['size'] = 0;
|
||||
for (var childId in parentController.fileInfos[id]['children']) {
|
||||
clearDescendantFolderSizeR(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reduceAncestorFolderSizeR(int id, int size) {
|
||||
int parentId = parentController.fileInfos[id]['parentId'];
|
||||
|
||||
if (parentId != -1) {
|
||||
parentController.fileInfos[parentId]['size'] -= size;
|
||||
reduceAncestorFolderSizeR(parentId, size);
|
||||
}
|
||||
}
|
||||
|
||||
int addDescendantFolderSizeR(int id) {
|
||||
if (parentController.fileInfos[id]['type'] == 'folder') {
|
||||
for (var childId in parentController.fileInfos[id]['children']) {
|
||||
parentController.fileInfos[id]['size'] +=
|
||||
addDescendantFolderSizeR(childId);
|
||||
}
|
||||
}
|
||||
return parentController.fileInfos[id]['size'];
|
||||
}
|
||||
|
||||
void addAncestorFolderSizeR(int id, int size) {
|
||||
int parentId = parentController.fileInfos[id]['parentId'];
|
||||
if (parentId != -1) {
|
||||
parentController.fileInfos[parentId]['size'] += size;
|
||||
addAncestorFolderSizeR(parentId, size);
|
||||
}
|
||||
}
|
||||
|
||||
List<fluent.TreeViewItem> buildTreeViewItemsR(int level, int parentId) {
|
||||
List<fluent.TreeViewItem> res = [];
|
||||
List children = parentController.fileInfos
|
||||
.where((e) => e['level'] == level && e['parentId'] == parentId)
|
||||
.toList();
|
||||
for (int j = 0; j < children.length; j++) {
|
||||
Map fileInfo = children[j];
|
||||
|
||||
if (fileInfo['type'] == 'folder') {
|
||||
// folder
|
||||
res.add(fluent.TreeViewItem(
|
||||
// expanded: false, bug on init
|
||||
value: fileInfo['id'],
|
||||
onInvoked: (item, reason) async {
|
||||
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
|
||||
List childrenIds = findChildFileIdsR(fileInfo['children']);
|
||||
if (item.selected == true) {
|
||||
parentController.selectedIndexes.addAll(childrenIds.cast());
|
||||
parentController.fileInfos[fileInfo['id']]['size'] = 0;
|
||||
addDescendantFolderSizeR(item.value);
|
||||
addAncestorFolderSizeR(item.value, fileInfo['size']);
|
||||
parentController.fileInfos.refresh();
|
||||
} else {
|
||||
parentController.selectedIndexes
|
||||
.removeWhere((index) => childrenIds.contains(index));
|
||||
reduceAncestorFolderSizeR(item.value, fileInfo['size']);
|
||||
clearDescendantFolderSizeR(item.value);
|
||||
|
||||
parentController.fileInfos.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
leading: Obx(() {
|
||||
return parentController.openedFolders.contains(fileInfo['id'])
|
||||
? const Icon(FaIcons.folder_open,
|
||||
color: Colors.lightBlueAccent)
|
||||
: const Icon(FaIcons.folder, color: Colors.lightBlueAccent);
|
||||
}),
|
||||
onExpandToggle: (fluent.TreeViewItem item, bool getExpanded) async {
|
||||
getExpanded
|
||||
? parentController.openedFolders.add(item.value)
|
||||
: parentController.openedFolders.remove(item.value);
|
||||
},
|
||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileInfo['name'],
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: context.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
return parentController.fileInfos[fileInfo['id']]['size'] != 0
|
||||
? Text(
|
||||
Util.fmtByte(
|
||||
parentController.fileInfos[fileInfo['id']]['size']),
|
||||
// style: context.textTheme.labelMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
})
|
||||
]),
|
||||
children: buildTreeViewItemsR(
|
||||
// parentController.fileInfos.where((e) => e['level'] > level).toList(),
|
||||
level + 1,
|
||||
fileInfo['id'])));
|
||||
} else {
|
||||
// file
|
||||
res.add(fluent.TreeViewItem(
|
||||
value: {'id': fileInfo['id'], 'fileId': fileInfo['fileId']},
|
||||
selected: fileInfo['selected'],
|
||||
onInvoked: (item, reason) async {
|
||||
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
|
||||
if (item.selected == true) {
|
||||
parentController.selectedIndexes.add(item.value['fileId']);
|
||||
addAncestorFolderSizeR(item.value['id'], fileInfo['size']);
|
||||
parentController.fileInfos.refresh();
|
||||
} else {
|
||||
parentController.selectedIndexes.remove(item.value['fileId']);
|
||||
reduceAncestorFolderSizeR(item.value['id'], fileInfo['size']);
|
||||
parentController.fileInfos.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
collapsable: false,
|
||||
leading: fileInfo['name'].lastIndexOf('.') == -1
|
||||
? Icon(
|
||||
FaIcons.doc,
|
||||
color: appController.downloaderConfig.value.extra.themeMode ==
|
||||
'dark'
|
||||
? Colors.white
|
||||
: Colors.black45,
|
||||
)
|
||||
: Icon(
|
||||
FaIcons.allIcons[findIcon(fileInfo['name'])],
|
||||
color: appController.downloaderConfig.value.extra.themeMode ==
|
||||
'dark'
|
||||
? Colors.white
|
||||
: Colors.black45,
|
||||
),
|
||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileInfo['name'],
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// style: context.textTheme.titleSmall,
|
||||
)),
|
||||
fileInfo['size'] > 0
|
||||
? Text(
|
||||
Util.fmtByte(
|
||||
fileInfo['size'],
|
||||
),
|
||||
// style: context.textTheme.labelMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
List<fluent.TreeViewItem> get items {
|
||||
List infos = [];
|
||||
int idNext = 0;
|
||||
List selectedFileIds = [];
|
||||
List openedFolders = [];
|
||||
//make fileInfos list
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
//parentId -1 means path root
|
||||
int parentId = -1;
|
||||
List folders = path.split(files[i].path);
|
||||
for (var folder in folders) {
|
||||
int indexInInfos = infos.lastIndexWhere((fileInfo) =>
|
||||
fileInfo['name'] == folder && fileInfo['parentId'] == parentId);
|
||||
|
||||
if (indexInInfos != -1) {
|
||||
// folder exists
|
||||
parentId = infos[indexInInfos]['id'];
|
||||
// add size
|
||||
infos[indexInInfos]['size'] += files[i].size;
|
||||
} else {
|
||||
// parent not exist
|
||||
// create one and add index to parent's children
|
||||
infos.add({
|
||||
'level': folders.indexOf(folder),
|
||||
'id': idNext,
|
||||
'type': 'folder',
|
||||
'name': folder,
|
||||
'size': files[i].size,
|
||||
'parentId': parentId,
|
||||
'children': [],
|
||||
});
|
||||
openedFolders.add(idNext);
|
||||
if (parentId != -1) {
|
||||
infos[parentId]['children'].add(idNext);
|
||||
}
|
||||
parentId = idNext;
|
||||
idNext++;
|
||||
}
|
||||
}
|
||||
//add one file, add index to parent
|
||||
infos.add({
|
||||
'id': idNext,
|
||||
'type': 'file',
|
||||
'fileId': i,
|
||||
'level': folders.length,
|
||||
'name': files[i].name,
|
||||
'size': files[i].size,
|
||||
'parentId': parentId,
|
||||
//TODO add unselected logic
|
||||
'selected': true
|
||||
});
|
||||
selectedFileIds.add(i);
|
||||
if (parentId != -1) {
|
||||
infos[parentId]['children'].add(idNext);
|
||||
}
|
||||
idNext++;
|
||||
}
|
||||
parentController.fileInfos.value = infos;
|
||||
parentController.selectedIndexes.value = selectedFileIds.cast();
|
||||
parentController.openedFolders.value = openedFolders;
|
||||
List<fluent.TreeViewItem> treeItems = buildTreeViewItemsR(0, -1);
|
||||
return treeItems;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return fluent.FluentTheme(
|
||||
data: appController.downloaderConfig.value.extra.themeMode == 'system'
|
||||
? fluent.FluentThemeData(brightness: ui.window.platformBrightness)
|
||||
: appController.downloaderConfig.value.extra.themeMode == 'light'
|
||||
? fluent.FluentThemeData(brightness: Brightness.light)
|
||||
: fluent.FluentThemeData(brightness: Brightness.dark),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
||||
Text(
|
||||
'selectFile'.tr,
|
||||
// style: TextStyle(color: themeData.hintColor),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey, width: 1),
|
||||
borderRadius: BorderRadius.circular(5)),
|
||||
child: fluent.TreeView(
|
||||
selectionMode: fluent.TreeViewSelectionMode.multiple,
|
||||
shrinkWrap: false,
|
||||
// addRepaintBoundaries: false,
|
||||
usePrototypeItem: true,
|
||||
// cacheExtent: 20,
|
||||
// onItemInvoked: (item, reason) async => {},
|
||||
onSecondaryTap: (item, details) async {
|
||||
debugPrint(
|
||||
'onSecondaryTap $item at ${details.globalPosition}');
|
||||
},
|
||||
// onSelectionChanged: (selectedItems) async => {},
|
||||
narrowSpacing: true,
|
||||
items: items,
|
||||
// scrollPrimary: true,
|
||||
))),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
import 'package:checkable_treeview/checkable_treeview.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
|
||||
import '../../api/model/resource.dart';
|
||||
import '../../icon/gopeed_icons.dart';
|
||||
import '../../util/util.dart';
|
||||
import 'file_icon.dart';
|
||||
import 'responsive_builder.dart';
|
||||
import 'sort_icon_button.dart';
|
||||
|
||||
const _toggleSwitchIcons = [
|
||||
Gopeed.file_video,
|
||||
Gopeed.file_audio,
|
||||
Gopeed.file_image,
|
||||
];
|
||||
const _sizeGapWidth = 72.0;
|
||||
|
||||
class FileTreeView extends StatefulWidget {
|
||||
final List<FileInfo> files;
|
||||
final List<int> initialValues;
|
||||
final Function(List<int>) onSelectionChanged;
|
||||
|
||||
const FileTreeView(
|
||||
{Key? key,
|
||||
required this.files,
|
||||
required this.initialValues,
|
||||
required this.onSelectionChanged})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<FileTreeView> createState() => _FileTreeViewState();
|
||||
}
|
||||
|
||||
class _FileTreeViewState extends State<FileTreeView> {
|
||||
late GlobalKey<TreeViewState<int>> key;
|
||||
late int totalSize;
|
||||
int? toggleSwitchIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
key = GlobalKey<TreeViewState<int>>();
|
||||
totalSize = widget.files
|
||||
.fold(0, (previousValue, element) => previousValue + element.size);
|
||||
widget.onSelectionChanged(widget.initialValues);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedFileCount =
|
||||
key.currentState?.getSelectedValues().where((e) => e != null).length ??
|
||||
widget.files.length;
|
||||
final selectedFileSize = calcSelectedSize(null);
|
||||
|
||||
final filterRow = InkWell(
|
||||
onTap: () {},
|
||||
child: ToggleSwitch(
|
||||
minHeight: 32,
|
||||
cornerRadius: 8,
|
||||
doubleTapDisable: true,
|
||||
inactiveBgColor: Theme.of(context).dividerColor,
|
||||
activeBgColor: [Theme.of(context).colorScheme.primary],
|
||||
initialLabelIndex: toggleSwitchIndex,
|
||||
icons: _toggleSwitchIcons,
|
||||
onToggle: (index) {
|
||||
toggleSwitchIndex = index;
|
||||
if (index == null) {
|
||||
key.currentState?.setSelectedValues(List.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
final iconFileExtArr = iconConfigMap[_toggleSwitchIcons[index]] ?? [];
|
||||
final selectedFileIndexes = widget.files
|
||||
.asMap()
|
||||
.entries
|
||||
.where((e) => iconFileExtArr.contains(fileExt(e.value.name)))
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
key.currentState?.setSelectedValues(selectedFileIndexes);
|
||||
},
|
||||
),
|
||||
);
|
||||
final countRow = Row(
|
||||
children: [
|
||||
Text('fileSelectedCount'.tr),
|
||||
Text(
|
||||
selectedFileCount.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('fileSelectedSize'.tr),
|
||||
Text(
|
||||
selectedFileCount > 0 && selectedFileSize == 0
|
||||
? 'unknown'.tr
|
||||
: Util.fmtByte(selectedFileSize),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: TreeView(
|
||||
key: key,
|
||||
nodes: buildTreeNodes(),
|
||||
showExpandCollapseButton: true,
|
||||
showSelectAll: true,
|
||||
onSelectionChanged: (selectedValues) {
|
||||
setState(() {});
|
||||
widget.onSelectionChanged(selectedValues
|
||||
.where((e) => e != null)
|
||||
.map((e) => e!)
|
||||
.toList());
|
||||
},
|
||||
selectAllTrailing: (context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text('name'.tr),
|
||||
SortIconButton(
|
||||
onStateChanged: (state) {
|
||||
switch (state) {
|
||||
case SortState.asc:
|
||||
key.currentState?.sort((p0, p1) {
|
||||
return (p0.label as Text)
|
||||
.data!
|
||||
.compareTo((p1.label as Text).data!);
|
||||
});
|
||||
break;
|
||||
case SortState.desc:
|
||||
key.currentState?.sort((p0, p1) {
|
||||
return (p1.label as Text)
|
||||
.data!
|
||||
.compareTo((p0.label as Text).data!);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
key.currentState?.sort(null);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text('size'.tr),
|
||||
SortIconButton(
|
||||
onStateChanged: (state) {
|
||||
switch (state) {
|
||||
case SortState.asc:
|
||||
key.currentState?.sort((p0, p1) {
|
||||
return calcSelectedSize(p0)
|
||||
.compareTo(calcSelectedSize(p1));
|
||||
});
|
||||
break;
|
||||
case SortState.desc:
|
||||
key.currentState?.sort((p0, p1) {
|
||||
return calcSelectedSize(p1)
|
||||
.compareTo(calcSelectedSize(p0));
|
||||
});
|
||||
break;
|
||||
default:
|
||||
key.currentState?.sort(null);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
!ResponsiveBuilder.isNarrow(context)
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
filterRow,
|
||||
countRow,
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
filterRow,
|
||||
const SizedBox(height: 8),
|
||||
countRow,
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
int calcSelectedSize(TreeNode<int>? node) {
|
||||
if (key.currentState == null) {
|
||||
return widget.files
|
||||
.fold(0, (previousValue, element) => previousValue + element.size);
|
||||
}
|
||||
|
||||
final selectedFileIndexes = node == null
|
||||
? key.currentState?.getSelectedValues()
|
||||
: (node.value != null
|
||||
? [node.value]
|
||||
: key.currentState?.getChildSelectedValues(node));
|
||||
|
||||
if (selectedFileIndexes == null) return 0;
|
||||
return selectedFileIndexes.where((e) => e != null).map((e) => e!).fold(0,
|
||||
(previousValue, element) => previousValue + widget.files[element].size);
|
||||
}
|
||||
|
||||
List<TreeNode<int>> buildTreeNodes() {
|
||||
final List<TreeNode<int>> rootNodes = [];
|
||||
final Map<String, TreeNode<int>> dirNodes = {};
|
||||
|
||||
for (var i = 0; i < widget.files.length; i++) {
|
||||
final file = widget.files[i];
|
||||
final parts = file.path.split('/');
|
||||
String currentPath = '';
|
||||
TreeNode<int>? parentNode;
|
||||
|
||||
// Create or get directory nodes
|
||||
for (final part in parts) {
|
||||
if (part.isEmpty) continue;
|
||||
|
||||
currentPath += '/$part';
|
||||
if (!dirNodes.containsKey(currentPath)) {
|
||||
final node = TreeNode<int>(
|
||||
label: Text(part),
|
||||
icon: Icon(
|
||||
fileIcon(part, isFolder: true),
|
||||
size: 18,
|
||||
),
|
||||
trailing: (context, node) {
|
||||
final size = calcSelectedSize(node);
|
||||
return size > 0
|
||||
? Text(Util.fmtByte(calcSelectedSize(node)),
|
||||
style: Theme.of(context).textTheme.bodySmall)
|
||||
: const SizedBox(width: _sizeGapWidth);
|
||||
},
|
||||
children: [],
|
||||
);
|
||||
dirNodes[currentPath] = node;
|
||||
|
||||
if (parentNode == null) {
|
||||
rootNodes.add(node);
|
||||
} else {
|
||||
parentNode.children.add(node);
|
||||
}
|
||||
}
|
||||
parentNode = dirNodes[currentPath];
|
||||
}
|
||||
|
||||
// Create file node using file.name
|
||||
final fileNode = TreeNode<int>(
|
||||
label: Text(file.name),
|
||||
value: i,
|
||||
icon: Icon(fileIcon(file.name, isFolder: false), size: 18),
|
||||
trailing: (context, node) {
|
||||
return file.size > 0
|
||||
? Text(Util.fmtByte(file.size),
|
||||
style: Theme.of(context).textTheme.bodySmall)
|
||||
: const SizedBox(width: _sizeGapWidth);
|
||||
},
|
||||
isSelected: widget.initialValues.contains(i),
|
||||
children: [],
|
||||
);
|
||||
|
||||
// Add file node to parent or root
|
||||
if (parentNode != null) {
|
||||
parentNode.children.add(fileNode);
|
||||
} else {
|
||||
rootNodes.add(fileNode);
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../icon/gopeed_icons.dart';
|
||||
|
||||
enum SortState { none, asc, desc }
|
||||
|
||||
class SortIconButton extends StatefulWidget {
|
||||
final double size;
|
||||
final Color? color;
|
||||
final Color? activeColor;
|
||||
final SortState initialState;
|
||||
final Function(SortState) onStateChanged;
|
||||
|
||||
const SortIconButton({
|
||||
Key? key,
|
||||
this.size = 18.0,
|
||||
this.color,
|
||||
this.activeColor,
|
||||
this.initialState = SortState.none,
|
||||
required this.onStateChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SortIconButton> createState() => _SortIconState();
|
||||
}
|
||||
|
||||
class _SortIconState extends State<SortIconButton> {
|
||||
late SortState _currentState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentState = widget.initialState;
|
||||
}
|
||||
|
||||
void _toggleState() {
|
||||
setState(() {
|
||||
switch (_currentState) {
|
||||
case SortState.none:
|
||||
_currentState = SortState.asc;
|
||||
break;
|
||||
case SortState.asc:
|
||||
_currentState = SortState.desc;
|
||||
break;
|
||||
case SortState.desc:
|
||||
_currentState = SortState.none;
|
||||
break;
|
||||
}
|
||||
});
|
||||
widget.onStateChanged(_currentState);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: _toggleState,
|
||||
child: SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: _currentState == SortState.none
|
||||
? Icon(
|
||||
Gopeed.sort,
|
||||
size: widget.size,
|
||||
color: widget.color,
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: 0.5,
|
||||
child: Icon(
|
||||
Gopeed.sort,
|
||||
size: widget.size,
|
||||
color: _currentState == SortState.asc
|
||||
? (widget.activeColor ??
|
||||
Theme.of(context).colorScheme.primary)
|
||||
: widget.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
heightFactor: 0.5,
|
||||
child: Icon(
|
||||
Gopeed.sort,
|
||||
size: widget.size,
|
||||
color: _currentState == SortState.desc
|
||||
? (widget.activeColor ??
|
||||
Theme.of(context).colorScheme.primary)
|
||||
: widget.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'libgopeed_interface.dart';
|
||||
import 'start_config.dart';
|
||||
|
||||
class LibgopeedChannel implements LibgopeedInterface {
|
||||
static const _channel = MethodChannel('gopeed.com/libgopeed');
|
||||
|
||||
@override
|
||||
Future<int> start(StartConfig cfg) async {
|
||||
final port = await _channel.invokeMethod('start', {
|
||||
'cfg': jsonEncode(cfg),
|
||||
});
|
||||
return port as int;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
return await _channel.invokeMethod('stop');
|
||||
}
|
||||
}
|
||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user