Compare commits

..
7 Commits
Author SHA1 Message Date
liwei 4736a91904 fix: test 2024-12-21 16:40:11 +08:00
liwei 0a4d74a513 Merge branch 'main' into feat/ffi
# Conflicts:
#	pkg/download/extension.go
2024-12-21 15:58:04 +08:00
liwei a06549a11a add test 2024-12-21 15:57:10 +08:00
liwei 63296f6950 cgo invoke params 2024-12-05 22:15:12 +08:00
liwei 4b4c03fd90 fix: ffi instance be singleton 2024-12-02 23:14:10 +08:00
liwei 91dfd78b77 feat: direct call download api by cgo dart 2024-12-02 00:39:28 +08:00
liwei 467ada5500 feat: direct call download api by cgo 2024-11-26 23:32:54 +08:00
188 changed files with 7024 additions and 8108 deletions

No files matched your search

+28 -163
View File
@@ -1,181 +1,46 @@
name-template: "v$NEXT_PATCH_VERSION 🌈"
tag-template: "v$NEXT_PATCH_VERSION"
name-template: 'v$NEXT_PATCH_VERSION 🌈'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
- title: "🆕 Features"
- title: '🆕 Features'
labels:
- "feat"
- "feature"
- "enhancement"
- title: "🐛 Bug Fixes"
- 'feat'
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- "fix"
- "bugfix"
- "bug"
- title: "🔧 Performance Improvements"
- 'fix'
- 'bugfix'
- 'bug'
- title: '🔧 Performance Improvements'
labels:
- "perf"
- title: "🧪 Tests"
label: "test"
- title: "🧰 Maintenance"
label: "chore"
- title: "📖 Document"
label: "docs"
- title: "🚀 CI/CD"
label: "ci"
- title: "🌎 Internationalization"
label: "translation"
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
- 'perf'
- title: '🧪 Tests'
label: 'test'
- title: '🧰 Maintenance'
label: 'chore'
- title: '📖 Document'
label: 'docs'
- title: '🚀 CI/CD'
label: 'ci'
- title: '🌎 Internationalization'
label: 'translation'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
version-resolver:
major:
labels:
- "major"
- 'major'
minor:
labels:
- "minor"
- 'minor'
patch:
labels:
- "patch"
- 'patch'
default: patch
template: |
<table>
<tbody>
<tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td><code>Portable</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3">🍎 MacOS</td>
<td rowspan="3"><code>DMG</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
</tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
</tr>
</tbody>
</table>
# Release notes
$CHANGES
# 更新日志
$CHANGES
$CHANGES
+75 -214
View File
@@ -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.24"
FLUTTER_VERSION: "3.24.5"
GO_VERSION: "1.23"
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
@@ -52,11 +52,9 @@ jobs:
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host.exe github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater.exe github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter
$TAG = "v$env:VERSION"
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable"
flutter build windows
$mingw = "C:\Program Files\Git\mingw64\bin"
$system = "C:\Windows\System32"
$release = "build\windows\x64\runner\Release\"
@@ -70,7 +68,6 @@ jobs:
New-Item -Path build\windows\Output -ItemType Directory
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller"
cd build/windows
echo @"
; Script generated by the Inno Setup Script Wizard.
@@ -100,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"
@@ -116,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]
@@ -139,10 +134,10 @@ jobs:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/windows/Output/*
overwrite: true
build-macos-amd64-lib:
build-macos-arm64-lib:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-13
needs: [get-release]
runs-on: macos-latest
needs: [ get-release ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -153,22 +148,14 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: macos-amd64-lib
path: |
libgopeed.dylib
host
updater
name: macos-arm64-lib
path: libgopeed.dylib
build-macos:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-latest
strategy:
matrix:
channel: [ arm64, amd64, universal ]
needs: [get-release, build-macos-amd64-lib]
runs-on: macos-13
needs: [ get-release, build-macos-arm64-lib ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -184,49 +171,27 @@ jobs:
run: |
python3 -m pip install setuptools --break-system-packages
npm install -g appdmg
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
name: macos-amd64-lib
path: ui/flutter/lib-amd64
- if: ${{ matrix.channel == 'arm64' }}
name: Build Arm64 Libraries
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
- if: ${{ matrix.channel == 'amd64' }}
name: Build Amd64 Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/libgopeed.dylib
cp ui/flutter/lib-amd64/host ui/flutter/assets/exec/host
cp ui/flutter/lib-amd64/updater ui/flutter/assets/exec/updater
- if: ${{ matrix.channel == 'universal' }}
name: Build Universal Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
# arm64 lib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/.temp/libgopeed.dylib.arm64 github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/.temp/host.arm64 github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/.temp/updater.arm64 github.com/GopeedLab/gopeed/cmd/updater
# amd64 lib
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.amd64
cp ui/flutter/lib-amd64/host ui/flutter/.temp/host.amd64
cp ui/flutter/lib-amd64/updater ui/flutter/.temp/updater.amd64
# universal lib
lipo -create -output ui/flutter/macos/Frameworks/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.arm64 ui/flutter/.temp/libgopeed.dylib.amd64
lipo -create -output ui/flutter/assets/exec/host ui/flutter/.temp/host.arm64 ui/flutter/.temp/host.amd64
lipo -create -output ui/flutter/assets/exec/updater ui/flutter/.temp/updater.arm64 ui/flutter/.temp/updater.amd64
name: macos-arm64-lib
path: ui/flutter/lib-arm64
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
cd ui/flutter
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
PROJECT_DIR=$(pwd)
# amd64 lib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
# universal binary
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
cd ui/flutter/macos/Frameworks
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
cd $PROJECT_DIR/ui/flutter
flutter build macos
cd build/macos/Build/Products/Release
cat>appdmg.json<<EOF
{
@@ -240,13 +205,6 @@ jobs:
EOF
mkdir dist
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
if [[ "${{ matrix.channel }}" == "arm64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-arm64.dmg
fi
if [[ "${{ matrix.channel }}" == "amd64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-amd64.dmg
fi
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -255,34 +213,23 @@ jobs:
overwrite: true
build-linux:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-22.04-arm]
needs: [get-release]
runs-on: ubuntu-latest
needs: [ get-release ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- if: matrix.os == 'ubuntu-22.04'
uses: subosito/flutter-action@v2
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- if: matrix.os == 'ubuntu-22.04-arm'
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: master
- run: |
sudo apt update -y
# flutter build dependencies
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
# appimage build dependencies
sudo apt install -y libfuse2 locate
arch=$(uname -m)
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/
- name: Build
@@ -290,42 +237,12 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter
cat << EOF > distribute_options.yaml
output: dist/
releases:
- name: linux
jobs:
- name: appimage
package:
platform: linux
target: appimage
build_args:
dart-define:
UPDATE_CHANNEL: linuxAppImage
- name: deb
package:
platform: linux
target: deb
build_args:
dart-define:
UPDATE_CHANNEL: linuxDeb
EOF
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
flutter_distributor release --name linux
flutter_distributor package --platform linux --targets appimage,deb
cd dist/*
# Get current running architecture, x86_64 -> amd64 and aarch64 -> arm64
ARCH="amd64"
if [[ "${{ matrix.os }}" == *-arm ]]; then
ARCH="arm64"
fi
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -335,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
@@ -350,45 +267,41 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
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: core24
base: core22
grade: stable
platforms:
amd64:
build-on: amd64
architectures:
- build-on: amd64
build-for: amd64
slots:
dbus-gopeed:
interface: dbus
bus: session
name: com.gopeed.gopeed
apps:
gopeed:
command-chain:
- bin/gpu-2404-wrapper
command: bin/gopeed
extensions: [gnome]
common-id: com.gopeed.Gopeed
plugs:
- network
- home
slots:
- dbus-gopeed
parts:
flutter-git:
source: https://github.com/flutter/flutter.git
@@ -428,37 +341,14 @@ jobs:
set +e
dart pub get
set -eux
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap"
flutter build linux --release -v
mkdir -p \$CRAFT_PART_INSTALL/bin/
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
gpu-2404:
after: [gopeed]
source: https://github.com/canonical/gpu-snap.git
plugin: dump
override-prime: |
craftctl default
\${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404
prime:
- bin/gpu-2404-wrapper
plugs:
gpu-2404:
interface: content
target: \$SNAP/gpu-2404
default-provider: mesa-2404
layout:
/usr/share/libdrm:
bind: \$SNAP/gpu-2404/libdrm
/usr/share/drirc.d:
symlink: \$SNAP/gpu-2404/drirc.d
/usr/share/X11/XErrorDB:
symlink: \$SNAP/gpu-2404/X11/XErrorDB
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
@@ -467,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
@@ -476,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 }}
@@ -493,13 +383,9 @@ jobs:
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk"
flutter build apk
mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
cp build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/Gopeed-v$VERSION-android-armeabi-v7a.apk
cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk dist/Gopeed-v$VERSION-android-arm64-v8a.apk
cp build/app/outputs/flutter-apk/app-x86_64-release.apk dist/Gopeed-v$VERSION-android-x86_64.apk
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
@@ -509,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
@@ -527,7 +413,7 @@ jobs:
gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa"
flutter build ios --no-codesign
mkdir Payload
cp -r build/ios/iphoneos/Runner.app Payload
zip -r -y Payload.zip Payload/Runner.app
@@ -540,9 +426,9 @@ jobs:
asset_path: ui/flutter/dist/*
overwrite: true
build-web:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' || github.event.inputs.platform == 'qnap' }}
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
@@ -555,12 +441,13 @@ jobs:
env:
VERSION: ${{ needs.get-release.outputs.version }}
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
@@ -582,75 +469,49 @@ jobs:
|| true
done
done
- uses: actions/upload-artifact@v4
with:
name: web-dist
path: dist/zip/
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: dist/zip/*
overwrite: true
build-qnap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'qnap' }}
runs-on: ubuntu-latest
needs: [get-release, build-web]
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.8.18"
- uses: actions/download-artifact@v4
with:
name: web-dist
path: dist/zip/
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
sudo apt update -y
sudo apt install -y pv bsdmainutils
sudo apt install -y pv python2 bsdmainutils
wget -O qdk2_0.32.bionic_amd64.deb "https://github.com/qnap-dev/qdk2/releases/download/v0.32/qdk2_0.32.bionic_amd64.deb"
dpkg -X qdk2_0.32.bionic_amd64.deb qdk2 # Direct installs will fail due to missing dependencies!
[[ -d qdk2 ]] || exit 1
export PATH=$(pwd)/qdk2/usr/bin:$(pwd)/qdk2/usr/share/qdk2/QDK/bin:${PATH}
wget -O Gopeed.template.tar.gz "https://github.com/GopeedLab/QpkgBuild/raw/refs/heads/master/template/Gopeed.template.tar.gz"
tar -zxf Gopeed.template.tar.gz
[[ -d Gopeed ]] || exit 1
goos=linux
goarch_arr=(amd64 arm64)
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
name=gopeed-web-v${VERSION}-${goos}-${goarch}
unzip dist/zip/${name}.zip -d dist/${name}
cp dist/${name}/${name}/* Gopeed/${qarch}/
done
cd Gopeed
sed -i -e 's/__QPKG_VER__/${VERSION}/g' qpkg.cfg
qbuild || exit 1
mkdir -p ../dist/qnap
for f in build/*; do echo ">>> build=[${f}]"; done
goos=qnap
goarch_arr=(amd64 arm64)
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
sname=Gopeed_${VERSION}_${qarch}.qpkg
dname=gopeed-v${VERSION}-${goos}-${goarch}.qpkg
[[ -f build/${sname} ]] && cp -ra build/${sname} ../dist/qnap/${dname}
dname=gopeed-${goos}-v${VERSION}-${goarch}.qpkg
echo "sname=${sname}, dname=${dname}"
[[ -f build/${sname} ]] && cp -ra build/${sname} ../dist/zip/${dname}
done
for f in ../dist/zip/*; do echo ">>> dist=[${f}]"; done
cd ..
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: dist/qnap/*
asset_path: dist/zip/*
overwrite: true
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: |
@@ -672,7 +533,7 @@ jobs:
- name: Build flutter web
run: |
cd ui/flutter
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker"
flutter build web --web-renderer html
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile"
env:
GO_VERSION: "1.24"
GO_VERSION: "1.23"
permissions:
contents: write
+8 -9
View File
@@ -27,8 +27,8 @@ on:
- "go.sum"
env:
GO_VERSION: "1.24"
FLUTTER_VERSION: "3.24.5"
GO_VERSION: "1.23"
FLUTTER_VERSION: "3.16.8"
jobs:
# lint:
@@ -53,7 +53,7 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests.
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/) -v -covermode=count -coverprofile=coverage.txt
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v4
with:
files: ./coverage.txt
@@ -63,7 +63,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
os: [ windows-latest, macos-latest, ubuntu-latest ]
needs: [ test ]
steps:
- uses: actions/checkout@v3
@@ -73,8 +73,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: master
- if: runner.os == 'Windows'
- 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
@@ -141,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
@@ -161,8 +160,8 @@ 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
docker build -t gopeed .
docker build -t gopeed .
+77 -149
View File
@@ -13,152 +13,80 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 🚀 Introduction
## Introduction
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
## ⬇️ Download
## Download
<table>
<tbody>
<tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td><code>Portable</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3">🍎 MacOS</td>
<td rowspan="3"><code>DMG</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
<thead>
<tr>
<th>Platform</th>
<th>Package Type</th>
<th>Download Link</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>Flathub</code></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
</tr>
</tbody>
</table>
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
### 🛠️ Command tool
### Command tool
use `go install`:
@@ -166,40 +94,40 @@ use `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
## 🔌 Browser Extension
## Browser Extension
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## 💝 Donate
## Donate
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
## 🖼️ Showcase
## Showcase
![](_docs/img/ui-demo.png)
## 👨‍💻 Development
## Development
This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
> The front code is located in the `ui/flutter` directory.
### 🌍 Environment
### Environment
1. Golang 1.23+
2. Flutter 3.24+
2. Flutter 3.16+
### 📋 Clone
### Clone
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### 🤝 Contributing
### Contributing
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
### 🏗️ Build
### Build
#### Desktop
@@ -246,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
```
@@ -272,18 +200,18 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
```
## ❤️ Credits
## Credits
### 👥 Contributors
### Contributors
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### 🏢 JetBrains
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## 📄 License
## License
[GPLv3](LICENSE)
+73 -146
View File
@@ -13,182 +13,109 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 🚀 はじめに
## はじめに
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
## ⬇️ インストール
## インストール
<table>
<tbody>
<tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td><code>Portable</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3">🍎 MacOS</td>
<td rowspan="3"><code>DMG</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
<thead>
<tr>
<th>Platform</th>
<th>Package Type</th>
<th>Download Link</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>Flathub</code></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
</tr>
</tbody>
</table>
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
### 🛠️ コマンドツール
## 💝 寄付
## 寄付
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
## 🖼️ ショーケース
## ショーケース
![](_docs/img/ui-demo.png)
## 👨‍💻 開発
## 開発
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
> フロントコードは `ui/flutter` ディレクトリにあります。
### 🌍 環境
### 環境
1. Go 言語 1.23+
2. Flutter 3.24+
2. Flutter 3.16+
### 📋 クローン
### クローン
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### 🤝 コントリビュート
### コントリビュート
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
### 🏗️ ビルド
### ビルド
#### デスクトップ
@@ -235,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
```
@@ -261,7 +188,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
```
## ❤️ 感謝
## 感謝
### コントリビューター
+74 -145
View File
@@ -13,151 +13,80 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 🚀 Giới thiệu
## Giới thiệu
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
## ⬇️ Tải về
## Tải về
<table>
<tbody>
<tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td><code>Portable</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3">🍎 MacOS</td>
<td rowspan="3"><code>DMG</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
<thead>
<tr>
<th>Nền tảng</th>
<th>Loại gói</th>
<th>Liên kết tải xuống</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>Bộ cài đặt EXE</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Liên kết</a></td>
</tr>
<tr>
<td><code>ZIP Portable</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Liên kết</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>Bộ cài đặt DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Liên kết</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>Flathub</code></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Liên kết</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">Liên kết</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Liên kết</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Liên kết</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Liên kết</a></td>
</tr>
<tr>
<td>iOS</td>
<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>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Liên kết</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Liên kết</a></td>
</tr>
</tbody>
</table>
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
### 🛠️ Công cụ lệnh
### Command tool
Sử dụng `go install`:
@@ -165,36 +94,36 @@ Sử dụng `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
## 💝 Quyên góp
## Quyên góp
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
## 🖼️ Trưng bày
## Trưng bày
![](_docs/img/ui-demo.png)
## 👨‍💻 Development
## Development
Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`.
> Mã giao diện nằm trong thư mục `ui/flutter`.
### 🌍 Environment
### Environment
1. Golang 1.23+
2. Flutter 3.24+
2. Flutter 3.16+
### 📋 Clone
### Clone
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### 🤝 Đóng góp
### Đóng góp
Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md)
### 🏗️ Xây dựng
### Xây dựng
#### Desktop
@@ -241,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
```
@@ -267,7 +196,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
```
## ❤️ Tín dụng
## Tín dụng
### Người đóng góp
+75 -146
View File
@@ -13,151 +13,80 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 🚀 介绍
## 介绍
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
## ⬇️ 下载
## 下载
<table>
<tbody>
<tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td><code>Portable</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3">🍎 MacOS</td>
<td rowspan="3"><code>DMG</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
<thead>
<tr>
<th>运行平台</th>
<th>安装包类型</th>
<th>下载链接</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>Flathub</code></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
</tr>
</tbody>
</table>
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
### 🛠️ 命令行工具
### 命令行工具
使用`go install`安装:
@@ -165,40 +94,40 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
## 🔌 浏览器扩展
## 浏览器扩展
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## 💝 赞助
## 赞助
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
## 🖼️ 界面展示
## 界面展示
![](_docs/img/ui-demo.png)
## 👨‍💻 开发
## 开发
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
> 前端代码位于`ui/flutter`目录下。
### 🌍 环境要求
### 环境要求
1. Golang 1.23+
2. Flutter 3.24+
2. Flutter 3.16+
### 📋 克隆项目
### 克隆项目
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### 🤝 贡献代码
### 贡献代码
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
### 🏗️ 编译
### 编译
#### 桌面端
@@ -245,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
```
@@ -271,7 +200,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
```
## ❤️ 感谢
## 感谢
### 贡献者
+73 -145
View File
@@ -13,152 +13,80 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 🚀 簡介
## 簡介
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
## ⬇️ 下載
## 下載
<table>
<tbody>
<tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td><code>Portable</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3">🍎 MacOS</td>
<td rowspan="3"><code>DMG</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
</tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
<thead>
<tr>
<th>運行平台</th>
<th>安裝包類型</th>
<th>下載鏈接</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan=2>Windows</td>
<td><code>EXE Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
</tr>
<tr>
<td><code>Portable ZIP</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
</tr>
<tr>
<td>MacOS</td>
<td><code>DMG Installer</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td>
</tr>
<tr>
<td rowspan=4>Linux</td>
<td><code>Flathub</code></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
</tr>
<tr>
<td>Android</td>
<td><code>APK</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
</tr>
<tr>
<td>iOS</td>
<td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
</tr>
<tr>
<td>Web</td>
<td></td>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
</tr>
<tr>
<td>Docker</td>
<td></td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
</tr>
</tbody>
</table>
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
### 🛠️ 使用 CLI 安裝
### 使用CLI安裝
使用`go install`安裝:
@@ -166,36 +94,36 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
## 💝 贊助
## 贊助
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
## 🖼️ 軟體介面
## 軟體介面
![](_docs/img/ui-demo.png)
## 👨‍💻 開發
## 開發
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
> 前端代碼位於`ui/flutter`目錄內。
### 🌍 開發環境
### 開發環境
1. Golang 1.23+
2. Flutter 3.24+
2. Flutter 3.16+
### 📋 克隆項目
### 克隆項目
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### 🤝 協助開發
### 協助開發
請參考[協助指南](CONTRIBUTING_zh-TW.md)
### 🏗️ 編譯
### 編譯
#### 桌面端
@@ -242,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
```
@@ -268,7 +196,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
```
## ❤️ 感謝
## 感謝
### 貢獻者
+41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-125
View File
@@ -1,125 +0,0 @@
package main
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/pkg/browser"
"github.com/shirou/gopsutil/v4/process"
)
const identifier = "gopeed"
type Message struct {
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
type Response struct {
Code int `json:"code"`
Data any `json:"data,omitempty"`
Message string `json:"message,omitempty"`
}
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
"ping": func(params json.RawMessage) (data any, err error) {
processes, err := process.Processes()
if err != nil {
return false, err
}
for _, p := range processes {
name, err := p.Name()
if err != nil {
continue
}
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
return true, nil
}
}
return false, nil
},
"create": func(params json.RawMessage) (data any, err error) {
var strParams string
if err = json.Unmarshal(params, &strParams); err != nil {
return
}
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
return
},
}
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/host
func main() {
for {
// Read message length (first 4 bytes)
var length uint32
if err := binary.Read(os.Stdin, binary.NativeEndian, &length); err != nil {
if err == io.EOF {
// Connection closed by Chrome
return
}
sendError("Failed to read message length: " + err.Error())
return
}
// Read the message
input := make([]byte, length)
if _, err := io.ReadFull(os.Stdin, input); err != nil {
sendError("Failed to read message: " + err.Error())
return
}
// Parse message
var message Message
if err := json.Unmarshal(input, &message); err != nil {
sendError("Failed to parse message: " + err.Error())
return
}
// Handle request
var data any
var err error
if handler, ok := apiMap[message.Method]; ok {
data, err = handler(message.Params)
} else {
err = errors.New("Unknown method: " + message.Method)
}
if err != nil {
sendError(err.Error())
continue
}
sendResponse(0, data, "")
}
}
func sendResponse(code int, data interface{}, message string) {
response := Response{
Code: code,
Data: data,
Message: message,
}
// Encode response
responseBytes, err := json.Marshal(response)
if err != nil {
sendError("Failed to encode response: " + err.Error())
return
}
// Write message length
binary.Write(os.Stdout, binary.NativeEndian, uint32(len(responseBytes)))
// Write message
os.Stdout.Write(responseBytes)
}
func sendError(msg string) {
sendResponse(1, nil, msg)
}
+18 -14
View File
@@ -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)
}()
}
-116
View File
@@ -1,116 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"syscall"
"time"
"github.com/pkg/browser"
"github.com/pkg/errors"
)
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/updater
func main() {
pid := flag.Int("pid", 0, "PID of the process to update")
updateChannel := flag.String("channel", "", "Update channel")
packagePath := flag.String("asset", "", "Path to the package asset")
logPath := flag.String("log", "", "Log file path")
flag.Parse()
if *pid == 0 {
log.Println("Invalid PID")
os.Exit(1)
}
if *updateChannel == "" {
log.Println("Invalid update channel")
os.Exit(1)
}
if *logPath != "" {
logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err == nil {
defer logFile.Close()
log.SetOutput(logFile)
}
}
var (
restart bool
err error
)
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
log.Printf("Update failed: %v\n", err)
os.Exit(1)
}
// Restart the application
if restart {
browser.OpenURL("gopeed:///")
}
// Delete package asset
if *packagePath != "" {
os.Remove(*packagePath)
}
os.Exit(0)
}
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
killSignalChan := make(chan any, 1)
go func() {
<-killSignalChan
if err = killProcess(pid); err != nil {
log.Printf("Failed to kill process: %v\n", err)
}
if err = waitForProcessExit(pid); err != nil {
log.Printf("Failed to wait for process exit: %v\n", err)
}
}()
appDir := filepath.Dir(os.Args[0])
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
return false, errors.Wrap(err, "failed to install package")
}
return
}
func waitForProcessExit(pid int) error {
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
process, err := os.FindProcess(pid)
if err != nil {
// On some systems, error is returned if process doesn't exist
return nil
}
// Send null signal to test if process exists
err = process.Signal(syscall.Signal(0))
if err != nil {
// If error occurs, the process no longer exists
return nil
}
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("process %d still running after timeout", pid)
}
func killProcess(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {
return err
}
return process.Kill()
}
-68
View File
@@ -1,68 +0,0 @@
//go:build darwin
package main
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
return true, installByDmg(killSignalChan, packagePath, destDir)
}
// installByDmg handles macOS dmg package installation
func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error {
// /Applications/Gopeed.app/Contents/MacOS -> /Applications
appPath := getParentDir(getParentDir(getParentDir(destDir)))
output, err := exec.Command("hdiutil", "attach", packagePath, "-nobrowse").Output()
if err != nil {
return err
}
mountPoint := ""
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "/Volumes/") {
fields := strings.Fields(line)
if len(fields) > 2 {
mountPoint = fields[len(fields)-1]
break
}
}
}
if mountPoint == "" {
return fmt.Errorf("failed to get mount point")
}
// Detach the mounted DMG
defer exec.Command("hdiutil", "detach", mountPoint, "-quiet").Run()
matches, err := filepath.Glob(filepath.Join(mountPoint, "*.app"))
if err != nil {
return err
}
if len(matches) == 0 {
return fmt.Errorf("no .app found in dmg")
}
killSignalChan <- nil
// Copy the new app to the destination
// cp -Rf /Volumes/GoPeed/GoPeed.app /Applications
if err := exec.Command("cp", "-Rf", matches[0], appPath).Run(); err != nil {
return err
}
return nil
}
// Get parent directory safely, handling trailing separators
func getParentDir(path string) string {
// Remove trailing separators if they exist
path = strings.TrimRight(path, string(filepath.Separator))
// Now get the parent directory
return filepath.Dir(path)
}
-72
View File
@@ -1,72 +0,0 @@
//go:build linux
package main
import (
"fmt"
"os/exec"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
killSignalChan <- nil
switch updateChannel {
case "linuxDeb":
return true, installByDeb(packagePath)
case "linuxFlathub":
return true, installByFlathub()
case "linuxSnap":
return true, installBySnap()
default:
return false, fmt.Errorf("unsupported update channel for Linux: %s", updateChannel)
}
}
// executeInTerminal tries to execute a command in one of several common terminal emulators
func executeInTerminal(command string) error {
terminals := []string{
"gnome-terminal", // GNOME
"konsole", // KDE
"xfce4-terminal", // XFCE
"xterm", // X11
}
command = fmt.Sprintf(`echo "Starting update..." && echo "[CMD] %s" && %s`, command, command)
for _, term := range terminals {
if _, err := exec.LookPath(term); err == nil {
var cmd *exec.Cmd
switch term {
case "gnome-terminal", "xfce4-terminal":
cmd = exec.Command(term, "--", "bash", "-c", command)
case "konsole":
cmd = exec.Command(term, "-e", "bash", "-c", command)
case "xterm":
cmd = exec.Command(term, "-e", command)
}
if err := cmd.Start(); err == nil {
return nil
}
}
}
return fmt.Errorf("no suitable terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal or xterm")
}
// installByDeb installs the .deb package
func installByDeb(packagePath string) error {
command := fmt.Sprintf(`sudo dpkg -i "%s"`, packagePath)
return executeInTerminal(command)
}
// installByFlathub updates the application via Flathub
func installByFlathub() error {
command := "flatpak update com.gopeed.Gopeed -y"
return executeInTerminal(command)
}
// installBySnap updates the application via Snap
func installBySnap() error {
command := "sudo snap refresh gopeed"
return executeInTerminal(command)
}
-134
View File
@@ -1,134 +0,0 @@
//go:build windows
package main
import (
"archive/zip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
switch updateChannel {
case "windowsInstaller":
return false, installByInstaller(killSignalChan, packagePath, destDir)
default:
return true, installByPortable(killSignalChan, packagePath, destDir)
}
}
// installByInstaller extracts the installer from the zip file and runs it
func installByInstaller(killSignalChan chan<- any, packagePath, destDir string) error {
// Create a temp directory for extraction
tempDir, err := os.MkdirTemp("", "gopeed_update")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
// Extract the zip file
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()
// Find the installer file
var installerPath string
for _, file := range reader.File {
if file.FileInfo().IsDir() {
continue
}
// Extract file
path := filepath.Join(tempDir, file.Name)
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
srcFile, err := file.Open()
if err != nil {
dstFile.Close()
return err
}
_, err = io.Copy(dstFile, srcFile)
srcFile.Close()
dstFile.Close()
if err != nil {
return err
}
// If this is likely an installer (.exe, .msi), save its path
ext := strings.ToLower(filepath.Ext(file.Name))
if ext == ".exe" || ext == ".msi" {
installerPath = path
}
}
if installerPath == "" {
return fmt.Errorf("no installer found in the update package")
}
// Run the installer
cmd := exec.Command(installerPath)
if err := cmd.Start(); err != nil {
return err
}
killSignalChan <- nil
return nil
}
// installByPortable extracts the portable version to the destination directory
func installByPortable(killSignalChan chan<- any, packagePath, destDir string) error {
killSignalChan <- nil
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()
for _, file := range reader.File {
path := filepath.Join(destDir, file.Name)
if file.FileInfo().IsDir() {
os.MkdirAll(path, file.Mode())
continue
}
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
srcFile, err := file.Open()
if err != nil {
dstFile.Close()
return err
}
_, err = io.Copy(dstFile, srcFile)
srcFile.Close()
dstFile.Close()
if err != nil {
return err
}
}
return nil
}
+4 -4
View File
@@ -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
View File
@@ -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,
+1 -13
View File
@@ -24,16 +24,6 @@ require (
golang.org/x/sync v0.9.0
)
require (
github.com/ebitengine/purego v0.8.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
@@ -109,11 +99,9 @@ require (
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v4 v4.0.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.24.12
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
@@ -125,7 +113,7 @@ require (
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.27.0 // indirect
-25
View File
@@ -143,8 +143,6 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
@@ -189,8 +187,6 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
@@ -292,8 +288,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
@@ -369,8 +363,6 @@ github.com/pion/webrtc/v4 v4.0.2 h1:fBwm5/hqSUybrCWl0DDBSTDrpbkcgkqpeLmXw9CsBQA=
github.com/pion/webrtc/v4 v4.0.2/go.mod h1:moylBT2A4dNoEaYBCdV1nThM3TLwRHzWszIG+eSPaqQ=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -378,8 +370,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -422,8 +412,6 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -459,10 +447,6 @@ github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EU
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
@@ -474,8 +458,6 @@ github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgK
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@@ -561,7 +543,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -571,7 +552,6 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -582,18 +562,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+2 -4
View File
@@ -24,10 +24,8 @@ type DefaultFileController struct {
func NewController() *Controller {
return &Controller{
GetConfig: func(v any) {},
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) {
return requestProxy.ToHandler()
},
GetConfig: func(v any) {},
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
FileController: &DefaultFileController{},
}
}
+11 -30
View File
@@ -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"
@@ -100,6 +98,9 @@ func (f *Fetcher) initClient() (err error) {
}
func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
f.meta.Req = req
if err := f.addTorrent(req, false); err != nil {
return err
@@ -331,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 {
@@ -352,43 +353,23 @@ func (f *Fetcher) WaitUpload() (err error) {
}
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return
}
if err = f.initClient(); err != nil {
return
}
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
+2 -22
View File
@@ -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.
+118 -277
View File
@@ -8,7 +8,6 @@ import (
"fmt"
"io"
"mime"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
@@ -16,7 +15,6 @@ import (
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/GopeedLab/gopeed/internal/controller"
@@ -27,12 +25,6 @@ import (
"golang.org/x/sync/errgroup"
)
const (
connectTimeout = 15 * time.Second
readTimeout = 15 * time.Second
helpMinSize = 1 * 1024 * 1024
)
type RequestError struct {
Code int
Msg string
@@ -50,22 +42,10 @@ type chunk struct {
Begin int64
End int64
Downloaded int64
}
type connection struct {
Chunk *chunk
Downloaded int64
Completed bool
failed bool
retryTimes int
}
// get remain to download bytes
func (c *chunk) remain() int64 {
return c.End - c.Begin + 1 - c.Downloaded
}
func newChunk(begin int64, end int64) *chunk {
return &chunk{
Begin: begin,
@@ -78,11 +58,8 @@ type Fetcher struct {
config *config
doneCh chan error
meta *fetcher.FetcherMeta
connections []*connection
helpLock sync.Mutex
redirectURL string
redirectLock sync.Mutex
meta *fetcher.FetcherMeta
chunks []*chunk
file *os.File
cancel context.CancelFunc
@@ -126,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
@@ -166,29 +143,12 @@ func (f *Fetcher) Resolve(req *base.Request) error {
_, params, _ := mime.ParseMediaType(contentDisposition)
filename := params["filename"]
if filename != "" {
// Check if the filename is MIME encoded-word
if strings.HasPrefix(filename, "=?") {
decoder := new(mime.WordDecoder)
filename = strings.Replace(filename, "UTF8", "UTF-8", 1)
file.Name, _ = decoder.Decode(filename)
} else {
file.Name = filename
}
} else {
substr := "attachment; filename="
index := strings.Index(contentDisposition, substr)
if index != -1 {
file.Name = contentDisposition[index+len(substr):]
}
file.Name = filename
}
}
// get file filePath by URL
if file.Name == "" {
file.Name = path.Base(httpReq.URL.Path)
// Url decode
if file.Name != "" {
file.Name, _ = url.QueryUnescape(file.Name)
}
}
// unknown file filePath
if file.Name == "" || file.Name == "/" || file.Name == "." {
@@ -241,10 +201,9 @@ func (f *Fetcher) Start() (err error) {
return err
}
if f.connections == nil {
f.connections = f.splitConnection()
if f.chunks == nil {
f.chunks = f.splitChunk()
}
f.redirectURL = ""
f.fetch()
return
}
@@ -271,26 +230,16 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
}
func (f *Fetcher) Stats() any {
statsConnections := make([]*fhttp.StatsConnection, 0)
for _, connection := range f.connections {
statsConnections = append(statsConnections, &fhttp.StatsConnection{
Downloaded: connection.Downloaded,
Completed: connection.Completed,
Failed: connection.failed,
RetryTimes: connection.retryTimes,
})
}
return &fhttp.Stats{
Connections: statsConnections,
}
// todo http download health
return &fhttp.Stats{}
}
func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0)
if len(f.connections) > 0 {
if len(f.chunks) > 0 {
total := int64(0)
for _, connection := range f.connections {
total += connection.Downloaded
for _, chunk := range f.chunks {
total += chunk.Downloaded
}
p = append(p, total)
}
@@ -305,34 +254,19 @@ func (f *Fetcher) fetch() {
var ctx context.Context
ctx, f.cancel = context.WithCancel(context.Background())
f.eg, _ = errgroup.WithContext(ctx)
connectionErrs := make([]error, len(f.connections))
for i := 0; i < len(f.connections); i++ {
for i := 0; i < len(f.chunks); i++ {
i := i
f.eg.Go(func() error {
err := f.run(i, ctx)
// if canceled, fail fast
if errors.Is(err, context.Canceled) {
return err
}
connectionErrs[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 connectionErrs {
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 {
@@ -342,192 +276,117 @@ func (f *Fetcher) fetch() {
}()
}
func (f *Fetcher) run(index int, ctx context.Context) (err error) {
connection := f.connections[index]
connection.failed = false
connection.retryTimes = 0
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
chunk := f.chunks[index]
chunk.retryTimes = 0
var (
client = f.buildClient()
buf = make([]byte, 8192)
client = f.buildClient()
buf = make([]byte, 8192)
maxRetries = 3
)
// retry until all remain chunks failed
for {
// if chunk is completed, return
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
return
}
downloadChunk := func(chunk *chunk) (err error) {
// retry until all remain chunks failed
for {
// if chunk is completed, return
if f.meta.Res.Range && chunk.remain() <= 0 {
return nil
if chunk.retryTimes >= maxRetries {
if !f.meta.Res.Range {
return
}
// if all chunks failed, return
if connection.failed {
allFailed := true
for _, c := range f.connections {
if c.Completed {
continue
}
if !c.failed {
allFailed = false
break
}
}
if allFailed {
if connection.retryTimes >= 3 {
return
} else {
connection.retryTimes++
}
// 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
}
}
err = func() error {
var (
httpReq *http.Request
resp *http.Response
)
f.redirectLock.Lock()
if f.redirectURL != "" {
f.redirectLock.Unlock()
}
err = func() (err error) {
defer func() {
if f.redirectURL == "" {
if err == nil {
f.redirectURL = resp.Request.URL.String()
}
f.redirectLock.Unlock()
}
}()
httpReq, err = f.buildRequest(ctx, f.meta.Req)
var (
httpReq *http.Request
resp *http.Response
)
httpReq, err = f.buildRequest(ctx, f.meta.Req)
if err != nil {
return
}
if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
} else {
chunk.Downloaded = 0
}
err = func() error {
resp, err = client.Do(httpReq)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status)
return err
}
// Http request success, reset retry times
chunk.retryTimes = 0
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
if err != nil {
return
}
if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
} else {
chunk.Downloaded = 0
}
resp, err = client.Do(httpReq)
if err != nil {
return
}
return
}()
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status)
return err
}
connection.failed = false
reader := NewTimeoutReader(resp.Body, readTimeout)
for {
n, err := reader.Read(buf)
if n > 0 {
finished := false
if f.meta.Res.Range {
remain := chunk.remain()
// If downloaded bytes exceed the remain bytes, only write remain bytes
if remain < int64(n) {
n = int(remain)
finished = true
}
}
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
if err != nil {
return err
}
chunk.Downloaded += int64(n)
connection.Downloaded += int64(n)
if finished {
return nil
}
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
chunk.Downloaded += int64(n)
}
}()
if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
if err != nil {
if err == io.EOF {
return nil
}
return err
}
// retry request after 1 second
connection.failed = true
time.Sleep(time.Second)
continue
}
break
}
return
}
for {
if err = downloadChunk(connection.Chunk); err != nil {
return
}
// check this connection is completed
if !f.meta.Res.Range || !f.helpOtherConnection(connection) {
connection.Completed = true
return
}
}
}
func (f *Fetcher) helpOtherConnection(helper *connection) bool {
f.helpLock.Lock()
defer f.helpLock.Unlock()
// find the slowest connection
var maxRemainConnection *connection
var maxRemain int64
for _, r := range f.connections {
if r == helper || r.Completed {
return nil
}()
if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
}
// retry request after 1 second
chunk.retryTimes = chunk.retryTimes + 1
time.Sleep(time.Second)
continue
}
remain := r.Chunk.remain()
if remain > maxRemain && remain > helpMinSize {
maxRemainConnection = r
maxRemain = remain
// if chunk is completed, reset other not complete chunks retry times
if err == nil {
for _, c := range f.chunks {
if c != chunk && c.retryTimes > 0 {
c.retryTimes = 0
}
}
}
break
}
if maxRemainConnection == nil {
return false
}
// re-calculate the chunk range
helper.Chunk.Begin = maxRemainConnection.Chunk.End - maxRemainConnection.Chunk.remain()/2
helper.Chunk.End = maxRemainConnection.Chunk.End
helper.Chunk.Downloaded = 0
maxRemainConnection.Chunk.End = helper.Chunk.Begin - 1
return true
return
}
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
var reqUrl string
if f.redirectURL != "" {
reqUrl = f.redirectURL
} else {
reqUrl = req.URL
url, err := url.Parse(req.URL)
if err != nil {
return
}
var (
method string
body io.Reader
)
headers := http.Header{}
if req.Extra == nil {
method = http.MethodGet
@@ -552,57 +411,47 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
}
if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, reqUrl, body)
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
} else {
httpReq, err = http.NewRequest(method, reqUrl, body)
httpReq, err = http.NewRequest(method, url.String(), body)
}
if err != nil {
return
}
httpReq.Header = headers
// Override Host header
if host := headers.Get(base.HttpHeaderHost); host != "" {
httpReq.Host = host
}
return httpReq, nil
}
func (f *Fetcher) splitConnection() (connections []*connection) {
func (f *Fetcher) splitChunk() (chunks []*chunk) {
if f.meta.Res.Range {
optConnections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
// 每个连接平均需要下载的分块大小
chunkSize := f.meta.Res.Size / int64(optConnections)
connections = make([]*connection, optConnections)
for i := 0; i < optConnections; i++ {
chunkSize := f.meta.Res.Size / int64(connections)
chunks = make([]*chunk, connections)
for i := 0; i < connections; i++ {
var (
begin = chunkSize * int64(i)
end int64
)
if i == optConnections-1 {
if i == connections-1 {
// 最后一个分块需要保证把文件下载完
end = f.meta.Res.Size - 1
} else {
end = begin + chunkSize - 1
}
connections[i] = &connection{
Chunk: newChunk(begin, end),
}
chunk := newChunk(begin, end)
chunks[i] = chunk
}
} else {
// 只支持单连接下载
connections = make([]*connection, 1)
connections[0] = &connection{
Chunk: newChunk(0, 0),
}
chunks = make([]*chunk, 1)
chunks[0] = newChunk(0, 0)
}
return
}
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,
@@ -616,16 +465,8 @@ func (f *Fetcher) buildClient() *http.Client {
}
}
func decodeMangledString(mangled string) string {
rawBytes := make([]byte, 0, len(mangled)*3)
for _, r := range mangled {
rawBytes = append(rawBytes, byte(r))
}
return string(rawBytes)
}
type fetcherData struct {
Connections []*connection
Chunks []*chunk
}
type FetcherManager struct {
@@ -658,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 == "." {
@@ -681,7 +522,7 @@ func (fm *FetcherManager) DefaultConfig() any {
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
_f := f.(*Fetcher)
return &fetcherData{
Connections: _f.connections,
Chunks: _f.chunks,
}, nil
}
@@ -693,8 +534,8 @@ func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
fetcher.meta = meta
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
if len(fd.Connections) > 0 {
fetcher.connections = fd.Connections
if len(fd.Chunks) > 0 {
fetcher.chunks = fd.Chunks
}
return fetcher
}
-89
View File
@@ -11,7 +11,6 @@ import (
"net"
gohttp "net/http"
"net/url"
"strings"
"testing"
"time"
)
@@ -37,36 +36,6 @@ func TestFetcher_Resolve(t *testing.T) {
},
},
}, t)
testResolve(test.StartTestCustomServer, "encoded-word", &base.Resource{
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: "测试.zip",
Size: test.BuildSize,
},
},
}, t)
testResolve(test.StartTestCustomServer, "no-encode", &base.Resource{
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: "测试.zip",
Size: test.BuildSize,
},
},
}, t)
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
Size: 0,
Range: false,
Files: []*base.FileInfo{
{
Name: "测试.zip",
Size: 0,
},
},
}, t)
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
Size: 0,
Range: false,
@@ -90,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()
@@ -178,22 +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_DownloadOnBugFileServer(t *testing.T) {
listener := test.StartTestRangeBugServer()
defer listener.Close()
downloadNormal(listener, 1, t)
downloadNormal(listener, 4, t)
}
func TestFetcher_DownloadResume(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
@@ -256,33 +194,6 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
}
}
func TestFetcher_Stats(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
fetcher := doDownloadReady(buildConfigFetcher(config{
Connections: 16,
}), listener, 0, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
stats := fetcher.Stats().(*http.Stats)
if len(stats.Connections) != 16 {
t.Errorf("Stats() got = %v, want %v", len(stats.Connections), 16)
}
totalDownloaded := int64(0)
for _, conn := range stats.Connections {
totalDownloaded += conn.Downloaded
}
if totalDownloaded != test.BuildSize {
t.Errorf("Stats() got = %v, want %v", totalDownloaded, test.BuildSize)
}
}
func TestFetcherManager_ParseName(t *testing.T) {
type args struct {
u string
-40
View File
@@ -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
}
+70 -141
View File
@@ -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)
@@ -83,43 +82,13 @@ func StartTestCustomServer() net.Listener {
defer file.Close()
io.Copy(writer, file)
})
mux.HandleFunc("/encoded-word", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Disposition", "attachment; filename=\"=?UTF8?B?5rWL6K+VLnppcA==?=\"")
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
mux.HandleFunc("/%E6%B5%8B%E8%AF%95.zip", func(writer http.ResponseWriter, request *http.Request) {
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
mux.HandleFunc("/no-encode", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Disposition", "attachment; filename=测试.zip")
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux
})
}
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++
@@ -139,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") != "" {
@@ -163,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++
@@ -180,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() {
@@ -191,105 +160,68 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
writer.WriteHeader(403)
return
}
rangeFileHandle(
writer,
request,
nil,
func(file *os.File, n int64) {
slowCopyN(sl, writer, file, n, delay)
},
)
r := request.Header.Get("Range")
if r == "" {
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
writer.WriteHeader(200)
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
slowCopy(writer, file, delay)
} else {
// split range
s := strings.Split(r, "=")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
s = strings.Split(s[1], "-")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
start, err := strconv.ParseInt(s[0], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
end, err := strconv.ParseInt(s[1], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
if start < 0 || end < 0 || start > end {
writer.WriteHeader(400)
return
}
if end >= BuildSize {
end = BuildSize - 1
}
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
writer.Header().Set("Accept-Ranges", "bytes")
writer.WriteHeader(206)
file, err := os.Open(BuildFile)
if err != nil {
writer.WriteHeader(500)
return
}
defer file.Close()
file.Seek(start, 0)
slowCopyN(writer, file, end-start+1, delay)
}
})
return mux
})
}
// StartTestRangeBugServer simulate bug server:
// Don't follow Range request rules, always return more data than range, e.g. Range: bytes=0-100, return 150 bytes
func StartTestRangeBugServer() net.Listener {
return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
rangeFileHandle(
writer,
request,
func(end int64) int64 {
var bugEnd = end
if end != 0 {
bugEnd = end + 50
if bugEnd >= BuildSize {
bugEnd = end
}
}
return bugEnd
},
func(file *os.File, n int64) {
io.CopyN(writer, file, n)
},
)
})
return mux
})
}
func rangeFileHandle(writer http.ResponseWriter, request *http.Request, modifyEnd func(end int64) int64, iocpN func(file *os.File, n int64)) {
r := request.Header.Get("Range")
// split range
s := strings.Split(r, "=")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
s = strings.Split(s[1], "-")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
start, err := strconv.ParseInt(s[0], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
end, err := strconv.ParseInt(s[1], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
if start < 0 || end < 0 || start > end {
writer.WriteHeader(400)
return
}
if end >= BuildSize {
end = BuildSize - 1
}
if modifyEnd != nil {
end = modifyEnd(end)
}
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
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)
return
}
defer file.Close()
file.Seek(start, 0)
iocpN(file, end-start+1)
}
// 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])
@@ -318,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
}
@@ -330,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)
@@ -340,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)
@@ -357,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
View File
@@ -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()
}
+42
View File
@@ -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
View File
@@ -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))
}
+62 -177
View File
@@ -1,4 +1,4 @@
package rest
package api
import (
"bytes"
@@ -7,23 +7,22 @@ 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"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"time"
)
var (
restPort int
httpInstance *Instance
restPort int
taskReq = &base.Request{
Extra: map[string]any{
@@ -92,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()
}
@@ -119,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()
}
@@ -141,13 +140,11 @@ func TestCreateDirectTask(t *testing.T) {
func TestCreateDirectTaskBatch(t *testing.T) {
doTest(func() {
reqs := make([]*base.CreateTaskBatchItem, 0)
reqs := make([]*base.Request, 0)
for i := 0; i < 5; i++ {
reqs = append(reqs, &base.CreateTaskBatchItem{
Req: createReq.Req,
})
reqs = append(reqs, createReq.Req)
}
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
Reqs: reqs,
})
if len(taskIds) != len(reqs) {
@@ -156,50 +153,11 @@ func TestCreateDirectTaskBatch(t *testing.T) {
})
}
func TestCreateDirectTaskBatchWithOpt(t *testing.T) {
doTest(func() {
reqs := make([]*base.CreateTaskBatchItem, 0)
for i := 0; i < 5; i++ {
item := &base.CreateTaskBatchItem{
Req: createReq.Req,
}
if i == 0 {
item.Opts = &base.Options{
Name: "spe_opt.data",
}
}
reqs = append(reqs, item)
}
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
Reqs: reqs,
Opts: &base.Options{
Name: "default_opt.data",
},
})
if len(taskIds) != len(reqs) {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
}
for i, taskId := range taskIds {
task := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
if i == 0 {
if !strings.Contains(task.Name(), "spe_opt") {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "spe_opt.data")
}
} else {
if !strings.Contains(task.Name(), "default_opt") {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "default_opt.data")
}
}
}
})
}
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()
}
@@ -223,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()
@@ -257,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)
@@ -318,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()
}
@@ -344,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()
}
@@ -368,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()
}
@@ -397,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,
},
@@ -489,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)
})
}
@@ -521,66 +479,6 @@ func TestFsExtensionFail(t *testing.T) {
})
}
func TestWebFsEnhance(t *testing.T) {
indexHtml := `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>index</h1>
</body>
</html>
`
webDistPath := "dist"
os.MkdirAll("dist", os.ModePerm)
if err := os.WriteFile(filepath.Join(webDistPath, "index.html"), []byte(indexHtml), os.ModePerm); err != nil {
panic(err)
}
defer os.RemoveAll(webDistPath)
doTest0(func(cfg *model.StartConfig) {
cfg.WebFS = os.DirFS(webDistPath)
}, func() {
// First request no cache
code, header, _ := doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
"Accept-Encoding": "gzip",
}, nil)
if code != http.StatusOK {
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
}
// Check header last-modified
if _, ok := header["Last-Modified"]; !ok {
t.Errorf("TestWebFsEnhance() missing key = %v", "Last-Modified")
}
// Check gzip compress
if _, ok := header["Content-Encoding"]; !ok || header["Content-Encoding"] != "gzip" {
t.Errorf("TestWebFsEnhance() no gzip compress")
}
// Request with If-Modified-Since
ifModifiedSince := header["Last-Modified"]
code, _, _ = doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
"If-Modified-Since": ifModifiedSince,
}, nil)
if code != http.StatusNotModified {
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusNotModified)
}
// Request with un gzip
code, header, _ = doHttpRequest1(http.MethodGet, "/index.html?t=123", nil, nil)
if code != http.StatusOK {
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
}
// Check no gzip compress
if _, ok := header["Content-Encoding"]; ok && header["Content-Encoding"] == "gzip" {
t.Errorf("TestWebFsEnhance() has gzip compress")
}
})
}
func TestDoProxy(t *testing.T) {
doTest(func() {
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
@@ -609,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)
@@ -624,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)
@@ -635,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)
@@ -662,55 +572,37 @@ 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)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(),
"X-Api-Token": cfg.ApiToken,
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(),
"X-Api-Token": "",
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
}
func doTest(handler func()) {
doTest0(nil, handler)
}
func doTest0(onStart func(cfg *model.StartConfig), handler func()) {
testFunc := func(storage model.Storage) {
var cfg = &model.StartConfig{}
cfg.Init()
cfg.Storage = storage
cfg.StorageDir = ".test_storage"
cfg.WebEnable = true
if onStart != nil {
onStart(cfg)
cfg.DownloadConfig = &base.DownloaderStoreConfig{
Http: &base.DownloaderHttpConfig{
Enable: true,
},
}
fileListener := doStart(cfg)
httpInstance, _ = Create(cfg)
fileListener := doStart()
defer func() {
if err := fileListener.Close(); err != nil {
panic(err)
}
Stop()
Downloader.Clear()
httpInstance.StopHttp()
httpInstance.Clear()
}()
defer func() {
time.Sleep(500 * time.Millisecond)
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
@@ -720,21 +612,20 @@ func doTest0(onStart func(cfg *model.StartConfig), 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 doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
r1, _, r3 := doHttpRequest1(method, path, headers, body)
return r1, r3
func UnwrapResult[T any](result *model.Result[T]) T {
if result.HasError() {
panic(result.Msg)
}
return result.Data
}
func doHttpRequest1(method string, path string, headers map[string]string, body any) (int, map[string]string, []byte) {
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
var reader io.Reader
if body != nil {
buf, _ := json.Marshal(body)
@@ -755,18 +646,12 @@ func doHttpRequest1(method string, path string, headers map[string]string, body
panic(err)
}
defer response.Body.Close()
respHeader := make(map[string]string)
for k, vv := range response.Header {
respHeader[k] = vv[0]
}
respBody, err := io.ReadAll(response.Body)
if err != nil {
panic(err)
}
return response.StatusCode, respHeader, respBody
return response.StatusCode, respBody
}
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
@@ -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,
}
@@ -7,3 +7,8 @@ type CreateTask struct {
Req *base.Request `json:"req"`
Opt *base.Options `json:"opt"`
}
type CreateTaskBatch struct {
Reqs []*base.Request `json:"reqs"`
Opt *base.Options `json:"opt"`
}
-1
View File
@@ -15,7 +15,6 @@ const (
HttpCodeOK = 200
HttpCodePartialContent = 206
HttpHeaderHost = "Host"
HttpHeaderRange = "Range"
HttpHeaderAcceptRanges = "Accept-Ranges"
HttpHeaderContentLength = "Content-Length"
+23 -10
View File
@@ -163,16 +163,6 @@ func ParseOptsExtra[E any](opts *Options) error {
return nil
}
type CreateTaskBatch struct {
Reqs []*CreateTaskBatchItem `json:"reqs"`
Opts *Options `json:"opts"`
}
type CreateTaskBatchItem struct {
Req *Request `json:"req"`
Opts *Options `json:"opts"`
}
// DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
@@ -181,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"`
}
@@ -191,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{}
}
@@ -213,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
+8
View File
@@ -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,
},
+35 -39
View File
@@ -148,7 +148,6 @@ func (d *Downloader) Setup() error {
tasks = append(tasks[:i], tasks[i+1:]...)
continue
}
d.assignFetcherManager(task)
initTask(task)
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
task.Status = base.DownloadStatusPause
@@ -209,7 +208,7 @@ func (d *Downloader) Setup() error {
if task.Status == base.DownloadStatusRunning {
downloadDataChanged = current != task.Progress.Downloaded
task.Progress.Used = task.timer.Used()
task.Progress.Speed = task.updateSpeed(current-task.Progress.Downloaded, tick)
task.Progress.Speed = task.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick)
task.Progress.Downloaded = current
}
@@ -218,7 +217,7 @@ func (d *Downloader) Setup() error {
uploader := task.fetcher.(fetcher.Uploader)
currentUploaded := uploader.UploadedBytes()
uploadDataChanged = currentUploaded != task.Progress.Uploaded
task.Progress.UploadSpeed = task.updateUploadSpeed(currentUploaded-task.Progress.Uploaded, tick)
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
task.Progress.Uploaded = currentUploaded
}
d.emit(EventKeyProgress, task)
@@ -253,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()
@@ -359,14 +358,10 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
return d.doCreate(fetcher, opts)
}
func (d *Downloader) CreateDirectBatch(req *base.CreateTaskBatch) (taskId []string, err error) {
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
taskIds := make([]string, 0)
for _, ir := range req.Reqs {
opts := ir.Opts
if opts == nil {
opts = req.Opts
}
taskId, err := d.CreateDirect(ir.Req, opts.Clone())
for _, req := range reqs {
taskId, err := d.CreateDirect(req, opts.Clone())
if err != nil {
return nil, err
}
@@ -395,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
@@ -435,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
@@ -532,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)
@@ -577,28 +572,27 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
return
}
func (d *Downloader) deleteAll() (err error) {
var deleteTasksTemp []*Task
func (d *Downloader) deleteAll(force bool) (err error) {
deleteTasksPtr := make([]*Task, 0)
func() {
d.lock.Lock()
defer d.lock.Unlock()
for _, task := range d.tasks {
deleteTasksTemp = append(deleteTasksTemp, task)
}
deleteTasksPtr = append(deleteTasksPtr, d.tasks...)
d.tasks = make([]*Task, 0)
d.waitTasks = make([]*Task, 0)
}()
for _, task := range deleteTasksTemp {
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
@@ -728,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
}
@@ -739,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
}
@@ -750,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
}
@@ -835,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
}
@@ -901,7 +895,18 @@ func (d *Downloader) restoreTask(task *Task) error {
}
func (d *Downloader) restoreFetcher(task *Task) error {
v, f := task.fetcherManager.Restore()
var fm fetcher.FetcherManager
for _, f := range d.cfg.FetchManagers {
if f.Name() == task.Protocol {
fm = f
break
}
}
if fm == nil {
return ErrUnSupportedProtocol
}
task.fetcherManager = fm
v, f := fm.Restore()
if v != nil {
err := d.storage.Pop(bucketSave, task.ID, v)
if err != nil {
@@ -1143,15 +1148,6 @@ func logPanic(logDir string) {
debug.SetCrashOutput(f, debug.CrashOptions{})
}
func (d *Downloader) assignFetcherManager(task *Task) error {
fm, err := d.parseFm(task.Meta.Req.URL)
if err != nil {
return err
}
task.fetcherManager = fm
return nil
}
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
fm, err := d.parseFm(url)
if err != nil {
+27 -37
View File
@@ -137,15 +137,13 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
downloader.Clear()
}()
reqs := make([]*base.CreateTaskBatchItem, 0)
reqs := make([]*base.Request, 0)
fileNames := make([]string, 0)
for i := 0; i < 5; i++ {
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
reqs = append(reqs, &base.CreateTaskBatchItem{
Req: req,
})
reqs = append(reqs, req)
if i == 0 {
fileNames = append(fileNames, test.DownloadName)
} else {
@@ -162,14 +160,11 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
}
})
_, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
Reqs: reqs,
Opts: &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
})
if err != nil {
@@ -405,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)
}
@@ -429,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)
@@ -504,15 +499,13 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
downloader.Clear()
}()
reqs := make([]*base.CreateTaskBatchItem, 0)
reqs := make([]*base.Request, 0)
fileNames := make([]string, 0)
for i := 0; i < 10; i++ {
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
reqs = append(reqs, &base.CreateTaskBatchItem{
Req: req,
})
reqs = append(reqs, req)
if i == 0 {
fileNames = append(fileNames, test.DownloadName)
} else {
@@ -529,14 +522,11 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
}
})
taskIds, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
Reqs: reqs,
Opts: &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
})
if err != nil {
@@ -561,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))
@@ -570,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)
@@ -579,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)
@@ -588,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))
@@ -597,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)
@@ -606,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))
@@ -615,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)
@@ -624,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)
@@ -634,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)
+13 -9
View File
@@ -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},
})
}
+11 -22
View File
@@ -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
}
@@ -100,37 +100,26 @@ func (t *Task) clone() *Task {
return util.DeepClone(t)
}
func (t *Task) updateSpeed(downloaded int64, usedTime float64) int64 {
return calcSpeed(&t.speedArr, downloaded, usedTime)
}
func (t *Task) updateUploadSpeed(downloaded int64, usedTime float64) int64 {
return calcSpeed(&t.uploadSpeedArr, downloaded, usedTime)
}
func calcSpeed(speedArr *[]int64, downloaded int64, usedTime float64) int64 {
*speedArr = append(*speedArr, downloaded)
// Record last 5 seconds of download speed to calculate the average speed
if len(*speedArr) > int(5.0/usedTime) {
*speedArr = (*speedArr)[1:]
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
speedArr = append(speedArr, downloaded)
if len(speedArr) > 6 {
speedArr = speedArr[1:]
}
var total int64
for _, v := range *speedArr {
for _, v := range speedArr {
total += v
}
return int64(float64(total) / float64(len(*speedArr)) / usedTime)
return int64(float64(total) / float64(len(speedArr)) / usedTime)
}
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 {
+2 -8
View File
@@ -14,12 +14,6 @@ type OptsExtra struct {
// Stats for download
type Stats struct {
Connections []*StatsConnection `json:"connections"`
}
type StatsConnection struct {
Downloaded int64 `json:"downloaded"`
Completed bool `json:"completed"`
Failed bool `json:"failed"`
RetryTimes int `json:"retryTimes"`
// http stats
// health indicators of http
}
-389
View File
@@ -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 base.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)
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
}
-6
View File
@@ -1,6 +0,0 @@
package rest
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
-34
View File
@@ -1,34 +0,0 @@
package rest
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (g gzipResponseWriter) Write(b []byte) (int, error) {
return g.Writer.Write(b)
}
func gzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding")
gz := gzip.NewWriter(w)
defer gz.Close()
next.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r)
})
}
-317
View File
@@ -1,317 +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"
"io/fs"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
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(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
}
enableApiToken := startCfg.ApiToken != ""
enableBasicAuth := startCfg.WebEnable && startCfg.WebBasicAuth != nil
if enableApiToken || enableBasicAuth {
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if enableApiToken {
apiTokenHeader := r.Header["X-Api-Token"]
// If api token header is set, only check api token ignore basic auth
if len(apiTokenHeader) > 0 {
if apiTokenHeader[0] == startCfg.ApiToken {
h.ServeHTTP(w, r)
return
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
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))
}
type embedCacheFileSystem struct {
fs http.FileSystem
lastModTime time.Time
}
func newEmbedCacheFileSystem(fs http.FileSystem) *embedCacheFileSystem {
efs := &embedCacheFileSystem{
fs: fs,
lastModTime: time.Now(),
}
exe, err := os.Executable()
if err != nil {
return efs
}
fi, err := os.Stat(exe)
if err != nil {
return efs
}
efs.lastModTime = fi.ModTime()
return efs
}
func (e *embedCacheFileSystem) Open(name string) (http.File, error) {
file, err := e.fs.Open(name)
if err != nil {
return nil, err
}
return &embedFile{
File: file,
lastModTime: e.lastModTime,
}, nil
}
type embedFile struct {
http.File
lastModTime time.Time
}
type embedFileInfo struct {
fs.FileInfo
lastModTime time.Time
}
func (e *embedFileInfo) ModTime() time.Time {
return e.lastModTime
}
func (e *embedFile) Stat() (fs.FileInfo, error) {
fi, err := e.File.Stat()
if err != nil {
return nil, err
}
return &embedFileInfo{
FileInfo: fi,
lastModTime: e.lastModTime,
}, nil
}
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)
}
+1
View File
@@ -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 ""
}
+1 -6
View File
@@ -67,9 +67,4 @@ windows/flutter/generated_plugins.cmake
# Hive database files
database.hive
database.lock
assets/exec/host
assets/exec/host.exe
assets/exec/updater
assets/exec/updater.exe
database.lock
+2 -2
View File
@@ -44,11 +44,11 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.gopeed.gopeed"
applicationId "com.gopeed"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 21
targetSdkVersion 34
targetSdkVersion 29
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed.gopeed">
package="com.gopeed">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed.gopeed">
package="com.gopeed">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@@ -7,8 +7,6 @@
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
@@ -93,8 +91,7 @@
<!-- Add android:stopWithTask option only when necessary. -->
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false" />
android:stopWithTask="true" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@@ -1,4 +1,4 @@
package com.gopeed.gopeed
package com.gopeed
import androidx.annotation.NonNull
import com.gopeed.libgopeed.Libgopeed
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed.gopeed">
package="com.gopeed">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
+2 -8
View File
@@ -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
}
-7
View File
@@ -32,13 +32,6 @@ target 'Runner' do
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# share_handler addition start
target 'ShareExtension' do
inherit! :search_paths
pod "share_handler_ios_models", :path => ".symlinks/plugins/share_handler_ios/ios/Models"
end
# share_handler addition end
end
post_install do |installer|
+4 -244
View File
@@ -7,9 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
0C585CBA2D41E28900FF2EC0 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */; };
0C585CBD2D41E28900FF2EC0 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 0C585CBC2D41E28900FF2EC0 /* Base */; };
0C585CC12D41E28900FF2EC0 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -23,28 +20,7 @@
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
0C585CBF2D41E28900FF2EC0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0C585CB62D41E28900FF2EC0;
remoteInfo = ShareExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
0C585CC22D41E28900FF2EC0 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
0C585CC12D41E28900FF2EC0 /* ShareExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 8;
@@ -59,13 +35,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; };
0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
0C585CBC2D41E28900FF2EC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
0C585CBE2D41E28900FF2EC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0C585CC72D41E36800FF2EC0 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
0C585CC82D41E38D00FF2EC0 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
@@ -88,13 +58,6 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0C585CB42D41E28900FF2EC0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -109,17 +72,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0C585CB82D41E28900FF2EC0 /* ShareExtension */ = {
isa = PBXGroup;
children = (
0C585CC82D41E38D00FF2EC0 /* ShareExtension.entitlements */,
0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */,
0C585CBB2D41E28900FF2EC0 /* MainInterface.storyboard */,
0C585CBE2D41E28900FF2EC0 /* Info.plist */,
);
path = ShareExtension;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -136,7 +88,6 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
0C585CB82D41E28900FF2EC0 /* ShareExtension */,
97C146EF1CF9000F007C117D /* Products */,
ADF7F433B4BC23D6C18AB78B /* Pods */,
B4A1CBAC5AA50208793CFF34 /* Frameworks */,
@@ -147,7 +98,6 @@
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -155,7 +105,6 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
0C585CC72D41E36800FF2EC0 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -192,23 +141,6 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
0C585CB62D41E28900FF2EC0 /* ShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0C585CC62D41E28900FF2EC0 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = (
0C585CB32D41E28900FF2EC0 /* Sources */,
0C585CB42D41E28900FF2EC0 /* Frameworks */,
0C585CB52D41E28900FF2EC0 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ShareExtension;
productName = ShareExtension;
productReference = 0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -218,7 +150,6 @@
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
0C585CC22D41E28900FF2EC0 /* Embed Foundation Extensions */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */,
@@ -226,7 +157,6 @@
buildRules = (
);
dependencies = (
0C585CC02D41E28900FF2EC0 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@@ -239,13 +169,9 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
0C585CB62D41E28900FF2EC0 = {
CreatedOnToolsVersion = 15.4;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
@@ -266,20 +192,11 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
0C585CB62D41E28900FF2EC0 /* ShareExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0C585CB52D41E28900FF2EC0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0C585CBD2D41E28900FF2EC0 /* Base in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -367,14 +284,6 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
0C585CB32D41E28900FF2EC0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0C585CBA2D41E28900FF2EC0 /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -386,23 +295,7 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0C585CC02D41E28900FF2EC0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0C585CB62D41E28900FF2EC0 /* ShareExtension */;
targetProxy = 0C585CBF2D41E28900FF2EC0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
0C585CBB2D41E28900FF2EC0 /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
0C585CBC2D41E28900FF2EC0 /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
@@ -422,123 +315,6 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
0C585CC32D41E28900FF2EC0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
0C585CC42D41E28900FF2EC0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
0C585CC52D41E28900FF2EC0 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -593,10 +369,8 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
@@ -605,7 +379,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -724,10 +498,8 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
@@ -736,7 +508,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -749,10 +521,8 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
@@ -761,7 +531,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -772,16 +542,6 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
0C585CC62D41E28900FF2EC0 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0C585CC32D41E28900FF2EC0 /* Debug */,
0C585CC42D41E28900FF2EC0 /* Release */,
0C585CC52D41E28900FF2EC0 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
+49 -101
View File
@@ -1,106 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gopeed</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gopeed</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<!-- Add for share_handler start -->
<!-- The 'NSUserActivityTypes' key is only needed if you plan to use the recordSentMessage API
allowing for conversations to show up as direct share suggestions -->
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<!-- Uncomment below lines if you want to use a custom group id rather than the default. Set it
in Build Settings -> User-Defined -->
<!-- <key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string> -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photos can be shared to and used in this app</string>
<!-- Optional: Add/Customize for AirDrop support -->
<key>LSSupportsOpeningDocumentsInPlace</key>
<string>No</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>ShareHandler</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.file-url</string>
<string>public.image</string>
<string>public.text</string>
<string>public.movie</string>
<string>public.url</string>
<string>public.data</string>
</array>
</dict>
</array>
<!-- Add for share_handler end -->
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</plist>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.gopeed.gopeed</string>
</array>
</dict>
</plist>
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
-49
View File
@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Uncomment below lines if you want to use a custom group id rather than the default. Set it
in Build Settings -> User-Defined -->
<!-- <key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string> -->
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<!-- Add supported message intent if you support sharing to a specific conversation - start -->
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
</array>
<!-- Add supported message intent if you support sharing to a specific conversation
(registered via the recordSentMessage api call) - end -->
<key>NSExtensionActivationRule</key>
<!-- Comment or delete the TRUEPREDICATE NSExtensionActivationRule that only works in
development mode -->
<!-- <string>TRUEPREDICATE</string> -->
<!-- Add a new NSExtensionActivationRule. The rule below will allow sharing one or more file
of any type, url, or text content, You can modify these rules to your liking for which types
of share content, as well as how many your app can handle -->
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
$attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count > 0 ).@count
> 0 </string>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.gopeed.gopeed</string>
</array>
</dict>
</plist>
@@ -1,3 +0,0 @@
import share_handler_ios_models
class ShareViewController: ShareHandlerIosViewController {}
-234
View File
@@ -1,234 +0,0 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart';
import '../util/util.dart';
import 'model/create_task.dart';
import 'model/create_task_batch.dart';
import 'model/downloader_config.dart';
import 'model/extension.dart';
import 'model/install_extension.dart';
import 'model/request.dart';
import 'model/resolve_result.dart';
import 'model/result.dart';
import 'model/switch_extension.dart';
import 'model/task.dart';
import 'model/update_check_extension_resp.dart';
import 'model/update_extension_settings.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<String>> createTaskBatch(CreateTaskBatch createTaskBatch) async {
return _parse<List<String>>(
() => _client.dio.post("/api/v1/tasks/batch", data: createTaskBatch),
(data) => (data as List).map((e) => e as String).toList());
}
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)}";
}
+6
View File
@@ -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)}";
}
}
+18
View File
@@ -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();
}
@@ -1,39 +1,20 @@
import 'package:gopeed/api/model/extension.dart';
import 'package:gopeed/api/model/request.dart';
import 'package:json_annotation/json_annotation.dart';
import 'options.dart';
import 'request.dart';
part 'create_task_batch.g.dart';
@JsonSerializable(explicitToJson: true)
@JsonSerializable()
class CreateTaskBatch {
List<CreateTaskBatchItem>? reqs;
Options? opt;
List<Request> reqs;
Option? opt;
CreateTaskBatch({
this.reqs,
this.reqs = const [],
this.opt,
});
factory CreateTaskBatch.fromJson(
Map<String, dynamic> json,
) =>
factory CreateTaskBatch.fromJson(Map<String, dynamic> json) =>
_$CreateTaskBatchFromJson(json);
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
}
@JsonSerializable()
class CreateTaskBatchItem {
Request? req;
Options? opts;
CreateTaskBatchItem({
this.req,
this.opts,
});
factory CreateTaskBatchItem.fromJson(Map<String, dynamic> json) =>
_$CreateTaskBatchItemFromJson(json);
Map<String, dynamic> toJson() => _$CreateTaskBatchItemToJson(this);
}
@@ -9,15 +9,18 @@ part of 'create_task_batch.dart';
CreateTaskBatch _$CreateTaskBatchFromJson(Map<String, dynamic> json) =>
CreateTaskBatch(
reqs: (json['reqs'] as List<dynamic>?)
?.map((e) => CreateTaskBatchItem.fromJson(e as Map<String, dynamic>))
.toList(),
?.map((e) => Request.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
opt: json['opt'] == null
? null
: Options.fromJson(json['opt'] as Map<String, dynamic>),
: Option.fromJson(json['opt'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
final val = <String, dynamic>{};
final val = <String, dynamic>{
'reqs': instance.reqs,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
@@ -25,31 +28,6 @@ Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
}
}
writeNotNull('reqs', instance.reqs?.map((e) => e.toJson()).toList());
writeNotNull('opt', instance.opt?.toJson());
return val;
}
CreateTaskBatchItem _$CreateTaskBatchItemFromJson(Map<String, dynamic> json) =>
CreateTaskBatchItem(
req: json['req'] == null
? null
: Request.fromJson(json['req'] as Map<String, dynamic>),
opts: json['opts'] == null
? null
: Options.fromJson(json['opts'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateTaskBatchItemToJson(CreateTaskBatchItem instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('req', instance.req);
writeNotNull('opts', instance.opts);
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,
};
+23
View File
@@ -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);
}
+23
View File
@@ -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,
};
+5 -9
View File
@@ -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);
}
+5 -6
View File
@@ -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>{
+14 -27
View File
@@ -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);
}
+13 -22
View File
@@ -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>{
+17
View File
@@ -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!;
}
}
@@ -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,
};
@@ -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);
@@ -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,
};
Loaded 100 of 188 files, more files were not shown because too many files have changed in this diff. Show more