mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfad91dcab | ||
|
|
61a959adda | ||
|
|
f4b839fd11 | ||
|
|
969aeb5e0a | ||
|
|
68d42eb397 | ||
|
|
08603f9510 | ||
|
|
b450bf67bd | ||
|
|
03e26dbea8 | ||
|
|
046a3ac9f9 | ||
|
|
9c5aebaf36 | ||
|
|
a094715d69 | ||
|
|
69bb875172 | ||
|
|
589f10c7ef | ||
|
|
654e57e946 | ||
|
|
b94b99ae09 | ||
|
|
fba990a056 | ||
|
|
cb07ce58bc | ||
|
|
50beef7b08 | ||
|
|
bd39e4110f | ||
|
|
86efcb98c5 | ||
|
|
f3d9873395 | ||
|
|
ef228c1722 | ||
|
|
6863317e9d | ||
|
|
2b83a735cb | ||
|
|
447f599783 | ||
|
|
f986f6253d | ||
|
|
2e1b621250 | ||
|
|
d50947b2e6 | ||
|
|
520a72415c | ||
|
|
a372b67edb | ||
|
|
2d86b5c132 | ||
|
|
63b3144633 | ||
|
|
1b0178fed9 | ||
|
|
83febe0a00 | ||
|
|
987580d400 | ||
|
|
0919936050 | ||
|
|
acc7ed8f6b | ||
|
|
ccb3267fba | ||
|
|
1249493a8a | ||
|
|
107141bbe7 | ||
|
|
b329fee107 | ||
|
|
3ce95f21fd | ||
|
|
f54ed5df5a | ||
|
|
727b7edb84 | ||
|
|
f5891ee03d | ||
|
|
724276ac69 | ||
|
|
a3f559c8e5 | ||
|
|
edd189a8a8 | ||
|
|
c970378fd7 | ||
|
|
8b61343aef |
No files matched your search
+155
-28
@@ -1,46 +1,173 @@
|
||||
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>🍎 MacOS</td>
|
||||
<td><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 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
|
||||
+197
-80
@@ -4,17 +4,17 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: 'Build platform'
|
||||
description: "Build platform"
|
||||
required: true
|
||||
default: 'all'
|
||||
default: "all"
|
||||
test:
|
||||
description: 'Test mode'
|
||||
description: "Test mode"
|
||||
required: true
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.23"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
|
||||
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,9 +52,11 @@ 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
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable"
|
||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||
$system = "C:\Windows\System32"
|
||||
$release = "build\windows\x64\runner\Release\"
|
||||
@@ -68,6 +70,7 @@ 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.
|
||||
@@ -97,11 +100,13 @@ 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"
|
||||
@@ -111,7 +116,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]
|
||||
@@ -137,7 +142,7 @@ jobs:
|
||||
build-macos-arm64-lib:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-latest
|
||||
needs: [ get-release ]
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -148,14 +153,19 @@ 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
|
||||
- uses: actions/upload-artifact@v3
|
||||
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
|
||||
with:
|
||||
name: macos-arm64-lib
|
||||
path: libgopeed.dylib
|
||||
path: |
|
||||
libgopeed.dylib
|
||||
host
|
||||
updater
|
||||
build-macos:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-13
|
||||
needs: [ get-release, build-macos-arm64-lib ]
|
||||
needs: [get-release, build-macos-arm64-lib]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -171,7 +181,7 @@ jobs:
|
||||
run: |
|
||||
python3 -m pip install setuptools --break-system-packages
|
||||
npm install -g appdmg
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos-arm64-lib
|
||||
path: ui/flutter/lib-arm64
|
||||
@@ -183,15 +193,25 @@ jobs:
|
||||
|
||||
# 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
|
||||
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/host github.com/GopeedLab/gopeed/cmd/host
|
||||
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||
# 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
|
||||
cp ui/flutter/lib-amd64/host ui/flutter/macos/Frameworks/amd64-host
|
||||
cp ui/flutter/lib-arm64/host ui/flutter/macos/Frameworks/arm64-host
|
||||
cp ui/flutter/lib-amd64/updater ui/flutter/macos/Frameworks/amd64-updater
|
||||
cp ui/flutter/lib-arm64/updater ui/flutter/macos/Frameworks/arm64-updater
|
||||
cd ui/flutter/macos/Frameworks
|
||||
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
|
||||
lipo -create -output host amd64-host arm64-host
|
||||
lipo -create -output updater amd64-updater arm64-updater
|
||||
rm -rf amd64-lib arm64-lib amd64-host arm64-host amd64-updater arm64-updater
|
||||
mv host ../../assets/exec/host
|
||||
|
||||
cd $PROJECT_DIR/ui/flutter
|
||||
flutter build macos
|
||||
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||
cd build/macos/Build/Products/Release
|
||||
cat>appdmg.json<<EOF
|
||||
{
|
||||
@@ -213,23 +233,34 @@ jobs:
|
||||
overwrite: true
|
||||
build-linux:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-22.04, ubuntu-22.04-arm]
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
- if: matrix.os == 'ubuntu-22.04'
|
||||
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
|
||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
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"
|
||||
chmod +x appimagetool
|
||||
sudo mv appimagetool /usr/local/bin/
|
||||
- name: Build
|
||||
@@ -237,12 +268,42 @@ 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 package --platform linux --targets appimage,deb
|
||||
flutter_distributor release --name linux
|
||||
cd dist/*
|
||||
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
|
||||
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
|
||||
|
||||
# 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
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
@@ -252,7 +313,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
|
||||
@@ -267,41 +328,45 @@ 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: core22
|
||||
base: core24
|
||||
grade: stable
|
||||
|
||||
architectures:
|
||||
- build-on: amd64
|
||||
|
||||
platforms:
|
||||
amd64:
|
||||
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
|
||||
@@ -341,14 +406,37 @@ jobs:
|
||||
set +e
|
||||
dart pub get
|
||||
set -eux
|
||||
flutter build linux --release -v
|
||||
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap"
|
||||
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
|
||||
@@ -357,7 +445,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
|
||||
@@ -366,7 +454,7 @@ jobs:
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11"
|
||||
java-version: "17"
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
@@ -383,9 +471,13 @@ 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
|
||||
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
|
||||
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk"
|
||||
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:
|
||||
@@ -395,7 +487,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
|
||||
@@ -413,7 +505,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
|
||||
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa"
|
||||
mkdir Payload
|
||||
cp -r build/ios/iphoneos/Runner.app Payload
|
||||
zip -r -y Payload.zip Payload/Runner.app
|
||||
@@ -426,9 +518,9 @@ jobs:
|
||||
asset_path: ui/flutter/dist/*
|
||||
overwrite: true
|
||||
build-web:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' }}
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' || github.event.inputs.platform == 'qnap' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -441,13 +533,12 @@ 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 --web-renderer html
|
||||
flutter build web --no-web-resources-cdn
|
||||
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
|
||||
@@ -469,49 +560,75 @@ jobs:
|
||||
|| true
|
||||
done
|
||||
done
|
||||
sudo apt update -y
|
||||
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}
|
||||
cp dist/${name}/${name}/* Gopeed/${qarch}/
|
||||
done
|
||||
cd Gopeed
|
||||
sed -i -e 's/__QPKG_VER__/${VERSION}/g' qpkg.cfg
|
||||
qbuild || exit 1
|
||||
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-${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 ..
|
||||
- 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
|
||||
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
|
||||
goos=qnap
|
||||
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}
|
||||
done
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||
asset_path: dist/qnap/*
|
||||
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: |
|
||||
@@ -533,7 +650,7 @@ jobs:
|
||||
- name: Build flutter web
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --web-renderer html
|
||||
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker"
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
|
||||
@@ -28,7 +28,7 @@ on:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.23"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
|
||||
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-latest ]
|
||||
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -73,7 +73,8 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- if: runner.os == 'windows'
|
||||
channel: master
|
||||
- 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
|
||||
@@ -140,7 +141,7 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --web-renderer html
|
||||
flutter build web --no-web-resources-cdn
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
@@ -160,8 +161,8 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --web-renderer html
|
||||
flutter build web --no-web-resources-cdn
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
docker build -t gopeed .
|
||||
docker build -t gopeed .
|
||||
@@ -13,80 +13,144 @@
|
||||
|
||||
[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>
|
||||
<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>
|
||||
<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>🍎 MacOS</td>
|
||||
<td><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 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>
|
||||
</table>
|
||||
|
||||
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||
|
||||
### Command tool
|
||||
### 🛠️ Command tool
|
||||
|
||||
use `go install`:
|
||||
|
||||
@@ -94,40 +158,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
|
||||
|
||||

|
||||
|
||||
## 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.16+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### Clone
|
||||
### 📋 Clone
|
||||
|
||||
```bash
|
||||
git clone [email protected]:GopeedLab/gopeed.git
|
||||
```
|
||||
|
||||
### Contributing
|
||||
### 🤝 Contributing
|
||||
|
||||
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
||||
|
||||
### Build
|
||||
### 🏗️ Build
|
||||
|
||||
#### Desktop
|
||||
|
||||
@@ -174,7 +238,7 @@ command:
|
||||
- android
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -200,18 +264,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
|
||||
|
||||
[](https://www.jetbrains.com/?from=gopeed)
|
||||
|
||||
## License
|
||||
## 📄 License
|
||||
|
||||
[GPLv3](LICENSE)
|
||||
+138
-73
@@ -13,109 +13,174 @@
|
||||
|
||||
[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>
|
||||
<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>
|
||||
<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>🍎 MacOS</td>
|
||||
<td><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 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>
|
||||
</table>
|
||||
|
||||
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||
|
||||
## 寄付
|
||||
### 🛠️ コマンドツール
|
||||
|
||||
## 💝 寄付
|
||||
|
||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||
|
||||
## ショーケース
|
||||
## 🖼️ ショーケース
|
||||
|
||||

|
||||
|
||||
## 開発
|
||||
## 👨💻 開発
|
||||
|
||||
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
|
||||
|
||||
> フロントコードは `ui/flutter` ディレクトリにあります。
|
||||
|
||||
### 環境
|
||||
### 🌍 環境
|
||||
|
||||
1. Go 言語 1.23+
|
||||
2. Flutter 3.16+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### クローン
|
||||
### 📋 クローン
|
||||
|
||||
```bash
|
||||
git clone [email protected]:GopeedLab/gopeed.git
|
||||
```
|
||||
|
||||
### コントリビュート
|
||||
### 🤝 コントリビュート
|
||||
|
||||
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
|
||||
|
||||
### ビルド
|
||||
### 🏗️ ビルド
|
||||
|
||||
#### デスクトップ
|
||||
|
||||
@@ -162,7 +227,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -188,7 +253,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
|
||||
```
|
||||
|
||||
## 感謝
|
||||
## ❤️ 感謝
|
||||
|
||||
### コントリビューター
|
||||
|
||||
|
||||
+137
-74
@@ -13,80 +13,143 @@
|
||||
|
||||
[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>
|
||||
<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>
|
||||
<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>🍎 MacOS</td>
|
||||
<td><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 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>
|
||||
</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)
|
||||
|
||||
### Command tool
|
||||
### 🛠️ Công cụ lệnh
|
||||
|
||||
Sử dụng `go install`:
|
||||
|
||||
@@ -94,36 +157,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
|
||||
|
||||

|
||||
|
||||
## 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.16+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 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
|
||||
|
||||
@@ -170,7 +233,7 @@ command:
|
||||
- android
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -196,7 +259,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
|
||||
|
||||
|
||||
+138
-75
@@ -13,80 +13,143 @@
|
||||
|
||||
[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>
|
||||
<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>
|
||||
<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>🍎 MacOS</td>
|
||||
<td><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 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>
|
||||
</table>
|
||||
|
||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||
|
||||
### 命令行工具
|
||||
### 🛠️ 命令行工具
|
||||
|
||||
使用`go install`安装:
|
||||
|
||||
@@ -94,40 +157,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)以支持这个项目的发展,非常感谢!
|
||||
|
||||
## 界面展示
|
||||
## 🖼️ 界面展示
|
||||
|
||||

|
||||
|
||||
## 开发
|
||||
## 👨💻 开发
|
||||
|
||||
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
|
||||
|
||||
> 前端代码位于`ui/flutter`目录下。
|
||||
|
||||
### 环境要求
|
||||
### 🌍 环境要求
|
||||
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.16+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 克隆项目
|
||||
### 📋 克隆项目
|
||||
|
||||
```bash
|
||||
git clone [email protected]:GopeedLab/gopeed.git
|
||||
```
|
||||
|
||||
### 贡献代码
|
||||
### 🤝 贡献代码
|
||||
|
||||
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
|
||||
|
||||
### 编译
|
||||
### 🏗️ 编译
|
||||
|
||||
#### 桌面端
|
||||
|
||||
@@ -174,7 +237,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -200,7 +263,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
|
||||
```
|
||||
|
||||
## 感谢
|
||||
## ❤️ 感谢
|
||||
|
||||
### 贡献者
|
||||
|
||||
|
||||
+137
-73
@@ -13,80 +13,144 @@
|
||||
|
||||
[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>
|
||||
<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>
|
||||
<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>🍎 MacOS</td>
|
||||
<td><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 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>
|
||||
</table>
|
||||
|
||||
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||
|
||||
### 使用CLI安裝
|
||||
### 🛠️ 使用 CLI 安裝
|
||||
|
||||
使用`go install`安裝:
|
||||
|
||||
@@ -94,36 +158,36 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
```
|
||||
|
||||
## 贊助
|
||||
## 💝 贊助
|
||||
|
||||
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||
|
||||
## 軟體介面
|
||||
## 🖼️ 軟體介面
|
||||
|
||||

|
||||
|
||||
## 開發
|
||||
## 👨💻 開發
|
||||
|
||||
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
|
||||
|
||||
> 前端代碼位於`ui/flutter`目錄內。
|
||||
|
||||
### 開發環境
|
||||
### 🌍 開發環境
|
||||
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.16+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 克隆項目
|
||||
### 📋 克隆項目
|
||||
|
||||
```bash
|
||||
git clone [email protected]:GopeedLab/gopeed.git
|
||||
```
|
||||
|
||||
### 協助開發
|
||||
### 🤝 協助開發
|
||||
|
||||
請參考[協助指南](CONTRIBUTING_zh-TW.md)
|
||||
|
||||
### 編譯
|
||||
### 🏗️ 編譯
|
||||
|
||||
#### 桌面端
|
||||
|
||||
@@ -170,7 +234,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
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
|
||||
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
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -196,7 +260,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
|
||||
```
|
||||
|
||||
## 感謝
|
||||
## ❤️ 感謝
|
||||
|
||||
### 貢獻者
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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)
|
||||
}
|
||||
+13
-18
@@ -3,32 +3,27 @@ package main
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/bind"
|
||||
"github.com/GopeedLab/gopeed/pkg/api"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
)
|
||||
|
||||
func main() {}
|
||||
|
||||
//export Init
|
||||
func Init(cfg *C.char) *C.char {
|
||||
//export Start
|
||||
func Start(cfg *C.char) (int, *C.char) {
|
||||
var config model.StartConfig
|
||||
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
||||
return C.CString(err.Error())
|
||||
return 0, C.CString(err.Error())
|
||||
}
|
||||
if err := bind.Init(&config); err != nil {
|
||||
return C.CString(err.Error())
|
||||
config.ProductionMode = true
|
||||
realPort, err := rest.Start(&config)
|
||||
if err != nil {
|
||||
return 0, C.CString(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
return realPort, nil
|
||||
}
|
||||
|
||||
//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))
|
||||
//export Stop
|
||||
func Stop() {
|
||||
rest.Stop()
|
||||
}
|
||||
+12
-7
@@ -3,15 +3,20 @@ package libgopeed
|
||||
// #cgo LDFLAGS: -static-libstdc++
|
||||
import "C"
|
||||
import (
|
||||
"github.com/GopeedLab/gopeed/bind"
|
||||
"github.com/GopeedLab/gopeed/pkg/api"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
)
|
||||
|
||||
func Init(cfg *model.StartConfig) error {
|
||||
return bind.Init(cfg)
|
||||
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 Invoke(request *api.Request) string {
|
||||
return bind.Invoke(request)
|
||||
func Stop() {
|
||||
rest.Stop()
|
||||
}
|
||||
+4
-10
@@ -2,21 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
)
|
||||
|
||||
// only for local development
|
||||
func main() {
|
||||
cfg := &model.StartConfig{
|
||||
Storage: model.StorageBolt,
|
||||
DownloadConfig: &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
Host: "127.0.0.1",
|
||||
Port: 9999,
|
||||
},
|
||||
},
|
||||
Network: "tcp",
|
||||
Address: "127.0.0.1:9999",
|
||||
Storage: model.StorageBolt,
|
||||
WebEnable: true,
|
||||
}
|
||||
cmd.Start(cfg)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
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)
|
||||
}
|
||||
+14
-18
@@ -3,9 +3,10 @@ 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"
|
||||
@@ -17,22 +18,20 @@ var banner string
|
||||
|
||||
func Start(cfg *model.StartConfig) {
|
||||
fmt.Println(banner)
|
||||
instance, err := api.Create(cfg)
|
||||
srv, listener, err := rest.BuildServer(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
downloadCfgResult := instance.GetConfig()
|
||||
if downloadCfgResult.HasError() {
|
||||
panic(downloadCfgResult.Msg)
|
||||
downloadCfg, err := rest.Downloader.GetConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
downloadCfg := downloadCfgResult.Data
|
||||
if downloadCfg.FirstLoad {
|
||||
// Set default download config
|
||||
if cfg.DownloadConfig != nil {
|
||||
cfg.DownloadConfig.Merge(downloadCfg)
|
||||
// TODO Use PatchConfig
|
||||
instance.PutConfig(cfg.DownloadConfig)
|
||||
rest.Downloader.PutConfig(cfg.DownloadConfig)
|
||||
downloadCfg = cfg.DownloadConfig
|
||||
}
|
||||
|
||||
@@ -49,28 +48,25 @@ func Start(cfg *model.StartConfig) {
|
||||
}
|
||||
if downloadDir != "" {
|
||||
downloadCfg.DownloadDir = downloadDir
|
||||
instance.PutConfig(downloadCfg)
|
||||
rest.Downloader.PutConfig(downloadCfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
watchExit()
|
||||
|
||||
watchExit(instance)
|
||||
|
||||
port, start, err := api.ListenHttp(cfg.DownloadConfig.Http, instance)
|
||||
if err != nil {
|
||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("Server start success on http://%s:%d\n", cfg.DownloadConfig.Http.Host, port)
|
||||
start()
|
||||
}
|
||||
|
||||
func watchExit(instance *api.Instance) {
|
||||
func watchExit() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
fmt.Printf("Server is shutting down due to signal: %s\n", sig)
|
||||
instance.Close()
|
||||
rest.Downloader.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//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)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//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)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//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
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type args struct {
|
||||
Host *string `json:"host"`
|
||||
Address *string `json:"address"`
|
||||
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.Host = flag.String("H", "0.0.0.0", "Bind Host")
|
||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
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.Host == nil {
|
||||
cfgArgs.Host = cliArgs.Host
|
||||
if cfgArgs.Address == nil {
|
||||
cfgArgs.Address = cliArgs.Address
|
||||
}
|
||||
if cfgArgs.Port == nil {
|
||||
cfgArgs.Port = cliArgs.Port
|
||||
|
||||
+5
-12
@@ -5,9 +5,9 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/api/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -42,19 +42,12 @@ 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,
|
||||
|
||||
@@ -24,6 +24,16 @@ 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
|
||||
@@ -99,9 +109,11 @@ 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
|
||||
@@ -113,7 +125,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.27.0 // indirect
|
||||
golang.org/x/sys v0.28.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
|
||||
|
||||
@@ -143,6 +143,8 @@ 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=
|
||||
@@ -187,6 +189,8 @@ 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=
|
||||
@@ -288,6 +292,8 @@ 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=
|
||||
@@ -363,6 +369,8 @@ 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=
|
||||
@@ -370,6 +378,8 @@ 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=
|
||||
@@ -412,6 +422,8 @@ 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=
|
||||
@@ -447,6 +459,10 @@ 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=
|
||||
@@ -458,6 +474,8 @@ 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=
|
||||
@@ -543,6 +561,7 @@ 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=
|
||||
@@ -552,6 +571,7 @@ 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=
|
||||
@@ -562,13 +582,18 @@ 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=
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -332,7 +334,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 {
|
||||
@@ -359,17 +361,34 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||
schema := util.ParseSchema(req.URL)
|
||||
if schema == "MAGNET" {
|
||||
f.torrent, err = client.AddMagnet(req.URL)
|
||||
} else if schema == "DATA" {
|
||||
_, data := util.ParseDataUri(req.URL)
|
||||
buf := bytes.NewBuffer(data)
|
||||
} 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()
|
||||
}
|
||||
|
||||
var metaInfo *metainfo.MetaInfo
|
||||
metaInfo, err = metainfo.Load(buf)
|
||||
if err != nil {
|
||||
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") {
|
||||
return err
|
||||
}
|
||||
f.torrent, err = client.AddTorrent(metaInfo)
|
||||
} else {
|
||||
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -96,7 +97,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||
}
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
t.Errorf("Resolve Single File Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -136,10 +137,29 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
||||
}
|
||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
t.Errorf("Resolve Multi Files Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/test.unclean.torrent",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Resolve file scheme Torrent", func(t *testing.T) {
|
||||
file, _ := filepath.Abs("./testdata/test.unclean.torrent")
|
||||
uri := "file:///" + file
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: uri,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFetcherManager_ParseName(t *testing.T) {
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
@@ -25,6 +26,9 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const connectTimeout = 15 * time.Second
|
||||
const readTimeout = 15 * time.Second
|
||||
|
||||
type RequestError struct {
|
||||
Code int
|
||||
Msg string
|
||||
@@ -43,9 +47,15 @@ type chunk struct {
|
||||
End int64
|
||||
Downloaded int64
|
||||
|
||||
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,
|
||||
@@ -103,9 +113,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
|
||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||
// parse content length from Content-Range header, eg: bytes 0-1000/1001 or bytes 0-0/*
|
||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||
if contentTotal != "" {
|
||||
if contentTotal != "" && contentTotal != "*" {
|
||||
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -143,12 +153,23 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
_, params, _ := mime.ParseMediaType(contentDisposition)
|
||||
filename := params["filename"]
|
||||
if filename != "" {
|
||||
file.Name = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 == "." {
|
||||
@@ -254,19 +275,34 @@ func (f *Fetcher) fetch() {
|
||||
var ctx context.Context
|
||||
ctx, f.cancel = context.WithCancel(context.Background())
|
||||
f.eg, _ = errgroup.WithContext(ctx)
|
||||
chunkErrs := make([]error, len(f.chunks))
|
||||
for i := 0; i < len(f.chunks); i++ {
|
||||
i := i
|
||||
f.eg.Go(func() error {
|
||||
return f.fetchChunk(i, ctx)
|
||||
err := f.fetchChunk(i, ctx)
|
||||
// if canceled, fail fast
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
chunkErrs[i] = err
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := f.eg.Wait()
|
||||
// check if canceled
|
||||
if errors.Is(err, context.Canceled) {
|
||||
// error returned only if canceled, just return
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// check all fetch results, if any error, return
|
||||
for _, chunkErr := range chunkErrs {
|
||||
if chunkErr != nil {
|
||||
err = chunkErr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
f.file.Close()
|
||||
// Update file last modified time
|
||||
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
||||
@@ -278,34 +314,34 @@ func (f *Fetcher) fetch() {
|
||||
|
||||
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
chunk := f.chunks[index]
|
||||
chunk.failed = false
|
||||
chunk.retryTimes = 0
|
||||
|
||||
var (
|
||||
client = f.buildClient()
|
||||
buf = make([]byte, 8192)
|
||||
maxRetries = 3
|
||||
client = f.buildClient()
|
||||
buf = make([]byte, 8192)
|
||||
)
|
||||
// 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
|
||||
if f.meta.Res.Range && chunk.remain() <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if chunk.retryTimes >= maxRetries {
|
||||
if !f.meta.Res.Range {
|
||||
return
|
||||
}
|
||||
// check if all failed
|
||||
// if all chunks failed, return
|
||||
if chunk.failed {
|
||||
allFailed := true
|
||||
for _, c := range f.chunks {
|
||||
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
||||
if !c.failed {
|
||||
allFailed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allFailed {
|
||||
return
|
||||
if chunk.retryTimes >= 3 {
|
||||
return
|
||||
} else {
|
||||
chunk.retryTimes++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,16 +369,30 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||
return err
|
||||
}
|
||||
// Http request success, reset retry times
|
||||
chunk.retryTimes = 0
|
||||
chunk.failed = false
|
||||
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
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)
|
||||
|
||||
if finished {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
@@ -351,7 +401,6 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
// If canceled, do not retry
|
||||
@@ -359,18 +408,10 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
return
|
||||
}
|
||||
// retry request after 1 second
|
||||
chunk.retryTimes = chunk.retryTimes + 1
|
||||
chunk.failed = true
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
// 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
|
||||
}
|
||||
return
|
||||
@@ -419,6 +460,10 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
||||
return
|
||||
}
|
||||
httpReq.Header = headers
|
||||
// Override Host header
|
||||
if host := headers.Get(base.HttpHeaderHost); host != "" {
|
||||
httpReq.Host = host
|
||||
}
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
@@ -452,6 +497,9 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
|
||||
func (f *Fetcher) buildClient() *http.Client {
|
||||
transport := &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: connectTimeout,
|
||||
}).DialContext,
|
||||
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
||||
@@ -499,7 +547,7 @@ func (fm *FetcherManager) ParseName(u string) string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// get filePath by URL
|
||||
// Get filePath by URL
|
||||
name = path.Base(url.Path)
|
||||
// If file name is empty, use host name
|
||||
if name == "" || name == "/" || name == "." {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -36,6 +37,26 @@ 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, "%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,
|
||||
@@ -59,6 +80,21 @@ 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()
|
||||
@@ -132,6 +168,22 @@ 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()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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
|
||||
}
|
||||
+130
-70
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/armon/go-socks5"
|
||||
@@ -36,7 +37,7 @@ const (
|
||||
)
|
||||
|
||||
func StartTestFileServer() net.Listener {
|
||||
return startTestServer(func() http.Handler {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return http.FileServer(http.Dir(Dir))
|
||||
})
|
||||
}
|
||||
@@ -52,7 +53,7 @@ func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||
return startTestServer(func() http.Handler {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
return &SlowFileServer{
|
||||
delay: delay,
|
||||
handler: http.FileServer(http.Dir(Dir)),
|
||||
@@ -61,7 +62,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||
}
|
||||
|
||||
func StartTestCustomServer() net.Listener {
|
||||
return startTestServer(func() http.Handler {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
file, err := os.Open(BuildFile)
|
||||
@@ -82,13 +83,32 @@ 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)
|
||||
})
|
||||
return mux
|
||||
})
|
||||
}
|
||||
|
||||
func StartTestRetryServer() net.Listener {
|
||||
counter := 0
|
||||
return startTestServer(func() http.Handler {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
counter++
|
||||
@@ -108,7 +128,7 @@ func StartTestRetryServer() net.Listener {
|
||||
}
|
||||
|
||||
func StartTestPostServer() net.Listener {
|
||||
return startTestServer(func() http.Handler {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
||||
@@ -132,7 +152,7 @@ func StartTestPostServer() net.Listener {
|
||||
|
||||
func StartTestErrorServer() net.Listener {
|
||||
counter := 0
|
||||
return startTestServer(func() http.Handler {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
counter++
|
||||
@@ -149,7 +169,7 @@ func StartTestErrorServer() net.Listener {
|
||||
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
var connections atomic.Int32
|
||||
|
||||
return startTestServer(func() http.Handler {
|
||||
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
defer func() {
|
||||
@@ -160,68 +180,105 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
writer.WriteHeader(403)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
rangeFileHandle(
|
||||
writer,
|
||||
request,
|
||||
nil,
|
||||
func(file *os.File, n int64) {
|
||||
slowCopyN(sl, writer, file, n, 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(dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
||||
func slowCopy(sl *shutdownListener, 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])
|
||||
@@ -250,8 +307,8 @@ func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err err
|
||||
return written, err
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
if written == n {
|
||||
return n, nil
|
||||
}
|
||||
@@ -262,7 +319,7 @@ func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int6
|
||||
return
|
||||
}
|
||||
|
||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||
func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.Listener {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -272,7 +329,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
// 随机生成一个文件
|
||||
// Write random data
|
||||
l := int64(8192)
|
||||
buf := make([]byte, l)
|
||||
size := int64(0)
|
||||
@@ -289,21 +346,24 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||
size += l
|
||||
}
|
||||
server := &http.Server{}
|
||||
server.Handler = serverHandle()
|
||||
go server.Serve(listener)
|
||||
|
||||
return &shutdownListener{
|
||||
sl := &shutdownListener{
|
||||
server: server,
|
||||
Listener: listener,
|
||||
}
|
||||
server.Handler = serverHandle(sl)
|
||||
go server.Serve(listener)
|
||||
|
||||
return sl
|
||||
}
|
||||
|
||||
type shutdownListener struct {
|
||||
server *http.Server
|
||||
server *http.Server
|
||||
isShutdown bool
|
||||
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
@@ -1,400 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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
@@ -1,451 +0,0 @@
|
||||
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))
|
||||
}
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
HttpCodeOK = 200
|
||||
HttpCodePartialContent = 206
|
||||
|
||||
HttpHeaderHost = "Host"
|
||||
HttpHeaderRange = "Range"
|
||||
HttpHeaderAcceptRanges = "Accept-Ranges"
|
||||
HttpHeaderContentLength = "Content-Length"
|
||||
|
||||
@@ -171,7 +171,6 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -182,12 +181,6 @@ 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{}
|
||||
}
|
||||
@@ -210,28 +203,12 @@ func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *Downl
|
||||
if cfg.Extra == nil {
|
||||
cfg.Extra = beforeCfg.Extra
|
||||
}
|
||||
if cfg.Http == nil {
|
||||
cfg.Http = beforeCfg.Http
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = beforeCfg.Proxy
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type DownloaderHttpConfig struct {
|
||||
// Enable is the flag that enable http server
|
||||
Enable bool `json:"Enable"`
|
||||
// Host is the http server listen address.
|
||||
Host string `json:"host"`
|
||||
// Port is the http server listen port.
|
||||
Port int `json:"port"`
|
||||
// ApiToken is the auth token for http API
|
||||
ApiToken string `json:"apiToken"`
|
||||
// RunningPort is the http server real listen port.
|
||||
RunningPort int `json:"runningPort"`
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
// System is the flag that use system proxy
|
||||
|
||||
@@ -17,8 +17,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
@@ -30,8 +28,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 10,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
@@ -47,8 +43,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
@@ -62,8 +56,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Extra: make(map[string]any),
|
||||
Http: &DownloaderHttpConfig{},
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
},
|
||||
|
||||
+17
-20
@@ -208,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.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick)
|
||||
task.Progress.Speed = task.updateSpeed(current-task.Progress.Downloaded, tick)
|
||||
task.Progress.Downloaded = current
|
||||
}
|
||||
|
||||
@@ -217,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.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
|
||||
task.Progress.UploadSpeed = task.updateUploadSpeed(currentUploaded-task.Progress.Uploaded, tick)
|
||||
task.Progress.Uploaded = currentUploaded
|
||||
}
|
||||
d.emit(EventKeyProgress, task)
|
||||
@@ -252,7 +252,7 @@ func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fet
|
||||
ctl.GetConfig = func(v any) {
|
||||
d.getProtocolConfig(fm.Name(), v)
|
||||
}
|
||||
// get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||
// Get proxy config, task request proxy config has higher priority, then use global proxy config
|
||||
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||
if requestProxy == nil {
|
||||
return d.cfg.Proxy.ToHandler()
|
||||
@@ -390,7 +390,7 @@ func (d *Downloader) Pause(filter *TaskFilter) (err error) {
|
||||
return d.pauseAll()
|
||||
}
|
||||
|
||||
filter.NotStatus = []base.Status{base.DownloadStatusPause, base.DownloadStatusError, base.DownloadStatusDone}
|
||||
filter.NotStatuses = []base.Status{base.DownloadStatusPause, base.DownloadStatusError, base.DownloadStatusDone}
|
||||
pauseTasks := d.GetTasksByFilter(filter)
|
||||
if len(pauseTasks) == 0 {
|
||||
return ErrTaskNotFound
|
||||
@@ -430,7 +430,7 @@ func (d *Downloader) Continue(filter *TaskFilter) (err error) {
|
||||
return d.continueAll()
|
||||
}
|
||||
|
||||
filter.NotStatus = []base.Status{base.DownloadStatusRunning, base.DownloadStatusDone}
|
||||
filter.NotStatuses = []base.Status{base.DownloadStatusRunning, base.DownloadStatusDone}
|
||||
continueTasks := d.GetTasksByFilter(filter)
|
||||
if len(continueTasks) == 0 {
|
||||
return ErrTaskNotFound
|
||||
@@ -527,7 +527,7 @@ func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
|
||||
|
||||
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||
if filter == nil || filter.IsEmpty() {
|
||||
return d.deleteAll(force)
|
||||
return d.deleteAll()
|
||||
}
|
||||
|
||||
deleteTasks := d.GetTasksByFilter(filter)
|
||||
@@ -572,27 +572,24 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) deleteAll(force bool) (err error) {
|
||||
deleteTasksPtr := make([]*Task, 0)
|
||||
|
||||
func (d *Downloader) deleteAll() (err error) {
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
deleteTasksPtr = append(deleteTasksPtr, d.tasks...)
|
||||
d.tasks = make([]*Task, 0)
|
||||
d.waitTasks = make([]*Task, 0)
|
||||
}()
|
||||
|
||||
for _, task := range deleteTasksPtr {
|
||||
if err = d.doDelete(task, force); err != nil {
|
||||
for _, task := range d.tasks {
|
||||
if err = d.doDelete(task, true); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) GetTaskStats(id string) (sr any, err error) {
|
||||
func (d *Downloader) Stats(id string) (sr any, err error) {
|
||||
task := d.GetTask(id)
|
||||
if task == nil {
|
||||
return sr, ErrTaskNotFound
|
||||
@@ -722,10 +719,10 @@ func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||
}
|
||||
|
||||
idMatch := func(task *Task) bool {
|
||||
if len(filter.ID) == 0 {
|
||||
if len(filter.IDs) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, id := range filter.ID {
|
||||
for _, id := range filter.IDs {
|
||||
if task.ID == id {
|
||||
return true
|
||||
}
|
||||
@@ -733,10 +730,10 @@ func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||
return false
|
||||
}
|
||||
statusMatch := func(task *Task) bool {
|
||||
if len(filter.Status) == 0 {
|
||||
if len(filter.Statuses) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, status := range filter.Status {
|
||||
for _, status := range filter.Statuses {
|
||||
if task.Status == status {
|
||||
return true
|
||||
}
|
||||
@@ -744,10 +741,10 @@ func (d *Downloader) GetTasksByFilter(filter *TaskFilter) []*Task {
|
||||
return false
|
||||
}
|
||||
notStatusMatch := func(task *Task) bool {
|
||||
if len(filter.NotStatus) == 0 {
|
||||
if len(filter.NotStatuses) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, status := range filter.NotStatus {
|
||||
for _, status := range filter.NotStatuses {
|
||||
if task.Status == status {
|
||||
return false
|
||||
}
|
||||
@@ -829,7 +826,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 {
|
||||
if task.Meta.Res == nil || d.GetTask(task.ID) == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -400,7 +400,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Millisecond * 1001)
|
||||
err = downloader.Pause(&TaskFilter{ID: []string{id}})
|
||||
err = downloader.Pause(&TaskFilter{IDs: []string{id}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -424,7 +424,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
err = downloader.Continue(&TaskFilter{ID: []string{id}})
|
||||
err = downloader.Continue(&TaskFilter{IDs: []string{id}})
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -551,7 +551,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter ids", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
ID: taskIds,
|
||||
IDs: taskIds,
|
||||
})
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), len(reqs))
|
||||
@@ -560,7 +560,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter match ids", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
ID: []string{taskIds[0]},
|
||||
IDs: []string{taskIds[0]},
|
||||
})
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 1)
|
||||
@@ -569,7 +569,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match ids", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
ID: []string{"xxx"},
|
||||
IDs: []string{"xxx"},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 0)
|
||||
@@ -578,7 +578,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
Status: []base.Status{base.DownloadStatusDone},
|
||||
Statuses: []base.Status{base.DownloadStatusDone},
|
||||
})
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), len(reqs))
|
||||
@@ -587,7 +587,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
Status: []base.Status{base.DownloadStatusError},
|
||||
Statuses: []base.Status{base.DownloadStatusError},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), 0)
|
||||
@@ -596,7 +596,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter match notStatus", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
NotStatus: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
|
||||
NotStatuses: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
|
||||
})
|
||||
if len(tasks) != len(reqs) {
|
||||
t.Errorf("GetTasksByFilter match notStatus task got = %v, want %v", len(tasks), len(reqs))
|
||||
@@ -605,7 +605,7 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match notStatus", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
NotStatus: []base.Status{base.DownloadStatusDone},
|
||||
NotStatuses: []base.Status{base.DownloadStatusDone},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter not match notStatus task got = %v, want %v", len(tasks), 0)
|
||||
@@ -614,8 +614,8 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter match ids and status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
ID: []string{taskIds[0]},
|
||||
Status: []base.Status{base.DownloadStatusDone},
|
||||
IDs: []string{taskIds[0]},
|
||||
Statuses: []base.Status{base.DownloadStatusDone},
|
||||
})
|
||||
if len(tasks) != 1 {
|
||||
t.Errorf("GetTasksByFilter match ids and status task got = %v, want %v", len(tasks), 1)
|
||||
@@ -624,8 +624,8 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||
|
||||
t.Run("GetTasksByFilter not match ids and status", func(t *testing.T) {
|
||||
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||
ID: []string{taskIds[0]},
|
||||
Status: []base.Status{base.DownloadStatusError},
|
||||
IDs: []string{taskIds[0]},
|
||||
Statuses: []base.Status{base.DownloadStatusError},
|
||||
})
|
||||
if len(tasks) != 0 {
|
||||
t.Errorf("GetTasksByFilter not match ids and status task got = %v, want %v", len(tasks), 0)
|
||||
|
||||
@@ -702,38 +702,34 @@ 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
|
||||
|
||||
Meta *ExtensionTaskMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// ExtensionTaskMeta restricts extension scripts to only modify request info
|
||||
type ExtensionTaskMeta struct {
|
||||
Req *base.Request `json:"req"`
|
||||
*Task
|
||||
}
|
||||
|
||||
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,
|
||||
Meta: &ExtensionTaskMeta{
|
||||
Req: task.Meta.Req,
|
||||
},
|
||||
Task: newTask,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ExtensionTask) Continue() error {
|
||||
return t.download.Continue(&TaskFilter{
|
||||
ID: []string{t.task.ID},
|
||||
IDs: []string{t.ID},
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ExtensionTask) Pause() error {
|
||||
return t.download.Pause(&TaskFilter{
|
||||
ID: []string{t.task.ID},
|
||||
IDs: []string{t.ID},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+22
-11
@@ -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,26 +100,37 @@ func (t *Task) clone() *Task {
|
||||
return util.DeepClone(t)
|
||||
}
|
||||
|
||||
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
||||
speedArr = append(speedArr, downloaded)
|
||||
if len(speedArr) > 6 {
|
||||
speedArr = speedArr[1:]
|
||||
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:]
|
||||
}
|
||||
|
||||
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 {
|
||||
ID []string `json:"id"`
|
||||
Status []base.Status `json:"status"`
|
||||
NotStatus []base.Status `json:"notStatus"`
|
||||
IDs []string
|
||||
Statuses []base.Status
|
||||
NotStatuses []base.Status
|
||||
}
|
||||
|
||||
func (f *TaskFilter) IsEmpty() bool {
|
||||
return len(f.ID) == 0 && len(f.Status) == 0 && len(f.NotStatus) == 0
|
||||
return len(f.IDs) == 0 && len(f.Statuses) == 0 && len(f.NotStatuses) == 0
|
||||
}
|
||||
|
||||
type DownloaderConfig struct {
|
||||
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func Info(w http.ResponseWriter, r *http.Request) {
|
||||
info := map[string]any{
|
||||
"version": base.Version,
|
||||
"runtime": runtime.Version(),
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"inDocker": base.InDocker == "true",
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(info))
|
||||
}
|
||||
|
||||
func Resolve(w http.ResponseWriter, r *http.Request) {
|
||||
var req base.Request
|
||||
if ReadJson(r, w, &req) {
|
||||
rr, err := Downloader.Resolve(&req)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(rr))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTask
|
||||
if ReadJson(r, w, &req) {
|
||||
var (
|
||||
taskId string
|
||||
err error
|
||||
)
|
||||
if req.Rid != "" {
|
||||
taskId, err = Downloader.Create(req.Rid, req.Opt)
|
||||
} else if req.Req != nil {
|
||||
taskId, err = Downloader.CreateDirect(req.Req, req.Opt)
|
||||
} else {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(taskId))
|
||||
}
|
||||
}
|
||||
|
||||
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.CreateTaskBatch
|
||||
if ReadJson(r, w, &req) {
|
||||
if len(req.Reqs) == 0 {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(taskIds))
|
||||
}
|
||||
}
|
||||
|
||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseIdFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Pause(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func PauseTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Pause(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseIdFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Continue(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Downloader.Continue(filter); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseIdFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
force := r.FormValue("force")
|
||||
|
||||
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
force := r.FormValue("force")
|
||||
|
||||
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
if taskId == "" {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
task := Downloader.GetTask(taskId)
|
||||
if task == nil {
|
||||
WriteJson(w, model.NewErrorResult("task not found", model.CodeTaskNotFound))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(task))
|
||||
}
|
||||
|
||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||
filter, errResult := parseFilter(r)
|
||||
if errResult != nil {
|
||||
WriteJson(w, errResult)
|
||||
return
|
||||
}
|
||||
|
||||
tasks := Downloader.GetTasksByFilter(filter)
|
||||
WriteJson(w, model.NewOkResult(tasks))
|
||||
}
|
||||
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJson(w, model.NewOkResult(getServerConfig()))
|
||||
}
|
||||
|
||||
func PutConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var cfg base.DownloaderStoreConfig
|
||||
if ReadJson(r, w, &cfg) {
|
||||
if err := Downloader.PutConfig(&cfg); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.InstallExtension
|
||||
if ReadJson(r, w, &req) {
|
||||
var (
|
||||
installedExt *download.Extension
|
||||
err error
|
||||
)
|
||||
if req.DevMode {
|
||||
installedExt, err = Downloader.InstallExtensionByFolder(req.URL, true)
|
||||
} else {
|
||||
installedExt, err = Downloader.InstallExtensionByGit(req.URL)
|
||||
}
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(installedExt.Identity))
|
||||
}
|
||||
}
|
||||
|
||||
func GetExtensions(w http.ResponseWriter, r *http.Request) {
|
||||
list := Downloader.GetExtensions()
|
||||
WriteJson(w, model.NewOkResult(list))
|
||||
}
|
||||
|
||||
func GetExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
ext, err := Downloader.GetExtension(identity)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(ext))
|
||||
}
|
||||
|
||||
func UpdateExtensionSettings(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
var req model.UpdateExtensionSettings
|
||||
if ReadJson(r, w, &req) {
|
||||
if err := Downloader.UpdateExtensionSettings(identity, req.Settings); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func SwitchExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
var switchExtension model.SwitchExtension
|
||||
if ReadJson(r, w, &switchExtension) {
|
||||
if err := Downloader.SwitchExtension(identity, switchExtension.Status); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DeleteExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
if err := Downloader.DeleteExtension(identity); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func UpdateCheckExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
newVersion, err := Downloader.UpgradeCheckExtension(identity)
|
||||
if err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(&model.UpdateCheckExtensionResp{
|
||||
NewVersion: newVersion,
|
||||
}))
|
||||
}
|
||||
|
||||
func UpdateExtension(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
identity := vars["identity"]
|
||||
if err := Downloader.UpgradeExtension(identity); err != nil {
|
||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewNilResult())
|
||||
}
|
||||
|
||||
func DoProxy(w http.ResponseWriter, r *http.Request) {
|
||||
target := r.Header.Get("X-Target-Uri")
|
||||
if target == "" {
|
||||
writeError(w, "param invalid: X-Target-Uri")
|
||||
return
|
||||
}
|
||||
targetUrl, err := url.Parse(target)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
r.RequestURI = ""
|
||||
r.URL = targetUrl
|
||||
r.Host = targetUrl.Host
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for k, vv := range resp.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Set(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func GetStats(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
if taskId == "" {
|
||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
||||
return
|
||||
}
|
||||
statsResult, err := Downloader.Stats(taskId)
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJson(w, model.NewOkResult(statsResult))
|
||||
}
|
||||
|
||||
func parseIdFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
if taskId == "" {
|
||||
return nil, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)
|
||||
}
|
||||
|
||||
filter := &download.TaskFilter{
|
||||
IDs: []string{taskId},
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func parseFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return nil, model.NewErrorResult(err.Error())
|
||||
}
|
||||
|
||||
filter := &download.TaskFilter{
|
||||
IDs: r.Form["id"],
|
||||
Statuses: convertStatues(r.Form["status"]),
|
||||
NotStatuses: convertStatues(r.Form["notStatus"]),
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func convertStatues(statues []string) []base.Status {
|
||||
result := make([]base.Status, 0)
|
||||
for _, status := range statues {
|
||||
result = append(result, base.Status(status))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, msg string) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func getServerConfig() *base.DownloaderStoreConfig {
|
||||
cfg, _ := Downloader.GetConfig()
|
||||
return cfg
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package rest
|
||||
|
||||
type Config struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,6 @@ type SwitchExtension struct {
|
||||
Status bool `json:"status"`
|
||||
}
|
||||
|
||||
type UpgradeCheckExtensionResp struct {
|
||||
type UpdateCheckExtensionResp struct {
|
||||
NewVersion string `json:"newVersion"`
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package model
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type RespCode int
|
||||
|
||||
const (
|
||||
@@ -22,15 +20,6 @@ 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,
|
||||
@@ -44,14 +33,14 @@ func NewNilResult() *Result[any] {
|
||||
}
|
||||
}
|
||||
|
||||
func NewErrorResult[T any](msg string, code ...RespCode) *Result[T] {
|
||||
func NewErrorResult(msg string, code ...RespCode) *Result[any] {
|
||||
// if code is not provided, the default code is CodeError
|
||||
c := CodeError
|
||||
if len(code) > 0 {
|
||||
c = code[0]
|
||||
}
|
||||
|
||||
return &Result[T]{
|
||||
return &Result[any]{
|
||||
Code: c,
|
||||
Msg: msg,
|
||||
}
|
||||
@@ -14,10 +14,13 @@ 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
|
||||
@@ -28,6 +31,12 @@ 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
|
||||
}
|
||||
@@ -50,9 +59,3 @@ 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"`
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,317 @@
|
||||
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,4 +1,4 @@
|
||||
package api
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -7,13 +7,14 @@ 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"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -21,8 +22,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
httpInstance *Instance
|
||||
restPort int
|
||||
restPort int
|
||||
|
||||
taskReq = &base.Request{
|
||||
Extra: map[string]any{
|
||||
@@ -91,7 +91,7 @@ func TestCreateTask(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -118,7 +118,7 @@ func TestCreateDirectTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func TestPauseAndContinueTask(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
switch event.Key {
|
||||
case download.EventKeyFinally:
|
||||
wg.Done()
|
||||
@@ -215,7 +215,10 @@ func TestPauseAndContinueTask(t *testing.T) {
|
||||
|
||||
func TestPauseAllAndContinueALLTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
cfg := UnwrapResult(httpInstance.GetConfig())
|
||||
cfg, err := Downloader.GetConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
createAndPause := func() {
|
||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||
@@ -273,7 +276,7 @@ func TestDeleteAllTasks(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -299,7 +302,7 @@ func TestDeleteTasksByStatues(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(taskCount)
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -323,7 +326,7 @@ func TestGetTasks(t *testing.T) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
httpInstance.Listener(func(event *download.Event) {
|
||||
Downloader.Listener(func(event *download.Event) {
|
||||
if event.Key == download.EventKeyFinally {
|
||||
wg.Done()
|
||||
}
|
||||
@@ -352,10 +355,7 @@ func TestGetAndPutConfig(t *testing.T) {
|
||||
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
|
||||
cfg.DownloadDir = "./download"
|
||||
cfg.Extra = map[string]any{
|
||||
"serverConfig": &struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
"serverConfig": &Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 8080,
|
||||
},
|
||||
@@ -447,16 +447,16 @@ func TestDeleteExtension(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpgradeCheckExtension(t *testing.T) {
|
||||
func TestUpdateCheckExtension(t *testing.T) {
|
||||
doTest(func() {
|
||||
identity := httpRequestCheckOk[string](http.MethodPost, "/api/v1/extensions", installExtensionReq)
|
||||
resp := httpRequestCheckOk[*model.UpgradeCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/upgrade", nil)
|
||||
resp := httpRequestCheckOk[*model.UpdateCheckExtensionResp](http.MethodGet, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
// no new version
|
||||
if resp.NewVersion != "" {
|
||||
t.Errorf("UpgradeCheckExtension() got = %v, want %v", resp.NewVersion, "")
|
||||
t.Errorf("UpdateCheckExtension() got = %v, want %v", resp.NewVersion, "")
|
||||
}
|
||||
// force update
|
||||
httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/upgrade", nil)
|
||||
httpRequestCheckOk[any](http.MethodPost, "/api/v1/extensions/"+identity+"/update", nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -479,6 +479,66 @@ 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{
|
||||
@@ -507,19 +567,13 @@ func TestDoProxy(t *testing.T) {
|
||||
func TestApiToken(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.DownloadConfig = &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
ApiToken: "123456",
|
||||
},
|
||||
}
|
||||
httpInstance, _ = Create(cfg)
|
||||
fileListener := doStart()
|
||||
cfg.ApiToken = "123456"
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
httpInstance.Close()
|
||||
Stop()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
@@ -528,7 +582,7 @@ func TestApiToken(t *testing.T) {
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.DownloadConfig.Http.ApiToken,
|
||||
"X-Api-Token": cfg.ApiToken,
|
||||
}, nil)
|
||||
if status != http.StatusOK {
|
||||
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusOK)
|
||||
@@ -539,24 +593,18 @@ func TestApiToken(t *testing.T) {
|
||||
func TestAuthorization(t *testing.T) {
|
||||
var cfg = &model.StartConfig{}
|
||||
cfg.Init()
|
||||
cfg.DownloadConfig = &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
ApiToken: "123456",
|
||||
},
|
||||
}
|
||||
cfg.ApiToken = "123456"
|
||||
cfg.WebEnable = true
|
||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
||||
Username: "admin",
|
||||
Password: "123456",
|
||||
}
|
||||
httpInstance, _ = Create(cfg)
|
||||
fileListener := doStart()
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
httpInstance.Close()
|
||||
Stop()
|
||||
}()
|
||||
|
||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||
@@ -572,37 +620,54 @@ func TestAuthorization(t *testing.T) {
|
||||
}
|
||||
|
||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||
"X-Api-Token": cfg.DownloadConfig.Http.ApiToken,
|
||||
"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": 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
|
||||
cfg.DownloadConfig = &base.DownloaderStoreConfig{
|
||||
Http: &base.DownloaderHttpConfig{
|
||||
Enable: true,
|
||||
},
|
||||
if onStart != nil {
|
||||
onStart(cfg)
|
||||
}
|
||||
httpInstance, _ = Create(cfg)
|
||||
fileListener := doStart()
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
httpInstance.StopHttp()
|
||||
httpInstance.Clear()
|
||||
Stop()
|
||||
Downloader.Clear()
|
||||
}()
|
||||
defer func() {
|
||||
httpInstance.PauseTasks(nil)
|
||||
httpInstance.DeleteTasks(nil, true)
|
||||
Downloader.Pause(nil)
|
||||
Downloader.Delete(nil, true)
|
||||
os.RemoveAll(cfg.StorageDir)
|
||||
}()
|
||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||
@@ -612,20 +677,21 @@ func doTest(handler func()) {
|
||||
testFunc(model.StorageBolt)
|
||||
}
|
||||
|
||||
func doStart() net.Listener {
|
||||
port := UnwrapResult(httpInstance.StartHttp())
|
||||
func doStart(cfg *model.StartConfig) net.Listener {
|
||||
port, err := Start(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
restPort = port
|
||||
return test.StartTestFileServer()
|
||||
}
|
||||
|
||||
func UnwrapResult[T any](result *model.Result[T]) T {
|
||||
if result.HasError() {
|
||||
panic(result.Msg)
|
||||
}
|
||||
return result.Data
|
||||
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
||||
r1, _, r3 := doHttpRequest1(method, path, headers, body)
|
||||
return r1, r3
|
||||
}
|
||||
|
||||
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
||||
func doHttpRequest1(method string, path string, headers map[string]string, body any) (int, map[string]string, []byte) {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
buf, _ := json.Marshal(body)
|
||||
@@ -646,12 +712,18 @@ func doHttpRequest0(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, respBody
|
||||
return response.StatusCode, respHeader, respBody
|
||||
}
|
||||
|
||||
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
|
||||
@@ -10,7 +10,6 @@ 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 ""
|
||||
}
|
||||
|
||||
@@ -67,4 +67,9 @@ windows/flutter/generated_plugins.cmake
|
||||
|
||||
# Hive database files
|
||||
database.hive
|
||||
database.lock
|
||||
database.lock
|
||||
|
||||
assets/exec/host
|
||||
assets/exec/host.exe
|
||||
assets/exec/updater
|
||||
assets/exec/updater.exe
|
||||
@@ -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"
|
||||
applicationId "com.gopeed.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 29
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gopeed">
|
||||
package="com.gopeed.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">
|
||||
package="com.gopeed.gopeed">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
@@ -7,6 +7,8 @@
|
||||
<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
|
||||
@@ -91,7 +93,8 @@
|
||||
<!-- Add android:stopWithTask option only when necessary. -->
|
||||
<service
|
||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:stopWithTask="true" />
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.gopeed
|
||||
package com.gopeed.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">
|
||||
package="com.gopeed.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.
|
||||
|
||||
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
@@ -74,8 +74,14 @@ typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern __declspec(dllexport) char* Init(char* cfg);
|
||||
extern __declspec(dllexport) char* Invoke(char* req);
|
||||
|
||||
/* Return type for Start */
|
||||
struct Start_return {
|
||||
GoInt r0;
|
||||
char* r1;
|
||||
};
|
||||
extern struct Start_return Start(char* cfg);
|
||||
extern void Stop();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -32,6 +32,13 @@ 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|
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
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 */; };
|
||||
@@ -20,7 +23,28 @@
|
||||
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;
|
||||
@@ -35,7 +59,13 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -58,6 +88,13 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
0C585CB42D41E28900FF2EC0 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -72,6 +109,17 @@
|
||||
/* 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 = (
|
||||
@@ -88,6 +136,7 @@
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
0C585CB82D41E28900FF2EC0 /* ShareExtension */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
ADF7F433B4BC23D6C18AB78B /* Pods */,
|
||||
B4A1CBAC5AA50208793CFF34 /* Frameworks */,
|
||||
@@ -98,6 +147,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -105,6 +155,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C585CC72D41E36800FF2EC0 /* Runner.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
@@ -141,6 +192,23 @@
|
||||
/* 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" */;
|
||||
@@ -150,6 +218,7 @@
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
0C585CC22D41E28900FF2EC0 /* Embed Foundation Extensions */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */,
|
||||
@@ -157,6 +226,7 @@
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
0C585CC02D41E28900FF2EC0 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
@@ -169,9 +239,13 @@
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1430;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
0C585CB62D41E28900FF2EC0 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
@@ -192,11 +266,20 @@
|
||||
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;
|
||||
@@ -284,6 +367,14 @@
|
||||
/* 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;
|
||||
@@ -295,7 +386,23 @@
|
||||
};
|
||||
/* 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 = (
|
||||
@@ -315,6 +422,123 @@
|
||||
/* 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 = {
|
||||
@@ -369,8 +593,10 @@
|
||||
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;
|
||||
@@ -379,7 +605,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -498,8 +724,10 @@
|
||||
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;
|
||||
@@ -508,7 +736,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -521,8 +749,10 @@
|
||||
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;
|
||||
@@ -531,7 +761,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -542,6 +772,16 @@
|
||||
/* 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 = (
|
||||
|
||||
@@ -1,54 +1,106 @@
|
||||
<?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>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<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 -->
|
||||
</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/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,3 @@
|
||||
import share_handler_ios_models
|
||||
|
||||
class ShareViewController: ShareHandlerIosViewController {}
|
||||
@@ -0,0 +1,227 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:gopeed/api/model/extension.dart';
|
||||
import 'package:gopeed/api/model/install_extension.dart';
|
||||
import 'package:gopeed/api/model/switch_extension.dart';
|
||||
|
||||
import '../util/util.dart';
|
||||
import 'model/create_task.dart';
|
||||
import 'model/downloader_config.dart';
|
||||
import 'model/request.dart';
|
||||
import 'model/resolve_result.dart';
|
||||
import 'model/result.dart';
|
||||
import 'model/task.dart';
|
||||
import 'model/update_extension_settings.dart';
|
||||
import 'model/update_check_extension_resp.dart';
|
||||
|
||||
class _Client {
|
||||
static _Client? _instance;
|
||||
|
||||
late Dio dio;
|
||||
|
||||
_Client._internal();
|
||||
|
||||
factory _Client(String network, String address, String apiToken) {
|
||||
if (_instance == null) {
|
||||
_instance = _Client._internal();
|
||||
var dio = Dio();
|
||||
final isUnixSocket = network == 'unix';
|
||||
var baseUrl = 'http://127.0.0.1';
|
||||
if (!isUnixSocket) {
|
||||
if (Util.isWeb()) {
|
||||
baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : '';
|
||||
} else {
|
||||
baseUrl = 'http://$address';
|
||||
}
|
||||
}
|
||||
dio.options.baseUrl = baseUrl;
|
||||
dio.options.contentType = Headers.jsonContentType;
|
||||
dio.options.sendTimeout = const Duration(seconds: 5);
|
||||
dio.options.connectTimeout = const Duration(seconds: 5);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 60);
|
||||
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
|
||||
if (apiToken.isNotEmpty) {
|
||||
options.headers['X-Api-Token'] = apiToken;
|
||||
}
|
||||
handler.next(options);
|
||||
}));
|
||||
|
||||
_instance!.dio = dio;
|
||||
if (isUnixSocket) {
|
||||
(_instance!.dio.httpClientAdapter as IOHttpClientAdapter)
|
||||
.createHttpClient = () {
|
||||
final client = HttpClient();
|
||||
client.connectionFactory =
|
||||
(Uri uri, String? proxyHost, int? proxyPort) {
|
||||
return Socket.startConnect(
|
||||
InternetAddress(address, type: InternetAddressType.unix), 0);
|
||||
};
|
||||
return client;
|
||||
};
|
||||
}
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutException implements Exception {
|
||||
final String message;
|
||||
|
||||
TimeoutException(this.message);
|
||||
}
|
||||
|
||||
late _Client _client;
|
||||
|
||||
void init(String network, String address, String apiToken) {
|
||||
_client = _Client(network, address, apiToken);
|
||||
}
|
||||
|
||||
Future<T> _parse<T>(
|
||||
Future<Response> Function() fetch,
|
||||
T Function(dynamic json)? fromJsonT,
|
||||
) async {
|
||||
try {
|
||||
var resp = await fetch();
|
||||
fromJsonT ??= (json) => null as T;
|
||||
final result = Result<T>.fromJson(resp.data, fromJsonT);
|
||||
if (result.code == 0) {
|
||||
return result.data as T;
|
||||
} else {
|
||||
throw Exception(result);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.sendTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.connectionTimeout) {
|
||||
throw TimeoutException("request timeout");
|
||||
}
|
||||
throw Exception(Result(code: 1000, msg: e.message));
|
||||
}
|
||||
}
|
||||
|
||||
Future<ResolveResult> resolve(Request request) async {
|
||||
return _parse<ResolveResult>(
|
||||
() => _client.dio.post("/api/v1/resolve", data: request),
|
||||
(data) => ResolveResult.fromJson(data));
|
||||
}
|
||||
|
||||
Future<String> createTask(CreateTask createTask) async {
|
||||
return _parse<String>(
|
||||
() => _client.dio.post("/api/v1/tasks", data: createTask),
|
||||
(data) => data as String);
|
||||
}
|
||||
|
||||
Future<List<Task>> getTasks(List<Status> statuses) async {
|
||||
return _parse<List<Task>>(
|
||||
() => _client.dio.get(
|
||||
"/api/v1/tasks?${statuses.map((e) => "status=${e.name}").join("&")}"),
|
||||
(data) => (data as List).map((e) => Task.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
Future<void> pauseTask(String id) async {
|
||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/pause"), null);
|
||||
}
|
||||
|
||||
Future<void> continueTask(String id) async {
|
||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
||||
}
|
||||
|
||||
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||
return _parse(
|
||||
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
|
||||
"id": ids,
|
||||
}),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> continueAllTasks(List<String>? ids) async {
|
||||
return _parse(
|
||||
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
|
||||
"id": ids,
|
||||
}),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> deleteTask(String id, bool force) async {
|
||||
return _parse(
|
||||
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
||||
}
|
||||
|
||||
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
||||
return _parse(
|
||||
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
|
||||
"id": ids,
|
||||
"force": force,
|
||||
}),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<DownloaderConfig> getConfig() async {
|
||||
return _parse(() => _client.dio.get("/api/v1/config"),
|
||||
(data) => DownloaderConfig.fromJson(data));
|
||||
}
|
||||
|
||||
Future<void> putConfig(DownloaderConfig config) async {
|
||||
return _parse(() => _client.dio.put("/api/v1/config", data: config), null);
|
||||
}
|
||||
|
||||
Future<void> installExtension(InstallExtension installExtension) async {
|
||||
return _parse(
|
||||
() => _client.dio.post("/api/v1/extensions", data: installExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<List<Extension>> getExtensions() async {
|
||||
return _parse<List<Extension>>(() => _client.dio.get("/api/v1/extensions"),
|
||||
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
||||
}
|
||||
|
||||
Future<void> updateExtensionSettings(
|
||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||
return _parse(
|
||||
() => _client.dio.put("/api/v1/extensions/$identity/settings",
|
||||
data: updateExtensionSettings),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> switchExtension(
|
||||
String identity, SwitchExtension switchExtension) async {
|
||||
return _parse(
|
||||
() => _client.dio
|
||||
.put("/api/v1/extensions/$identity/switch", data: switchExtension),
|
||||
null);
|
||||
}
|
||||
|
||||
Future<void> deleteExtension(String identity) async {
|
||||
return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null);
|
||||
}
|
||||
|
||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
||||
return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"),
|
||||
(data) => UpdateCheckExtensionResp.fromJson(data));
|
||||
}
|
||||
|
||||
Future<void> updateExtension(String identity) async {
|
||||
return _parse(
|
||||
() => _client.dio.post("/api/v1/extensions/$identity/update"), null);
|
||||
}
|
||||
|
||||
Future<Response<String>> proxyRequest<T>(String uri,
|
||||
{data, Options? options}) async {
|
||||
options ??= Options();
|
||||
options.headers ??= {};
|
||||
options.headers!["X-Target-Uri"] = uri;
|
||||
|
||||
// add timestamp to avoid cache
|
||||
return _client.dio.request(
|
||||
"/api/v1/proxy?t=${DateTime.now().millisecondsSinceEpoch}",
|
||||
data: data,
|
||||
options: options);
|
||||
}
|
||||
|
||||
String join(String path) {
|
||||
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}";
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
class ApiException implements Exception {
|
||||
final int code;
|
||||
final String message;
|
||||
|
||||
ApiException(this.code, this.message);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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 ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
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)}";
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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,20 +0,0 @@
|
||||
import 'package:gopeed/api/model/extension.dart';
|
||||
import 'package:gopeed/api/model/request.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'create_task_batch.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class CreateTaskBatch {
|
||||
List<Request> reqs;
|
||||
Option? opt;
|
||||
|
||||
CreateTaskBatch({
|
||||
this.reqs = const [],
|
||||
this.opt,
|
||||
});
|
||||
|
||||
factory CreateTaskBatch.fromJson(Map<String, dynamic> json) =>
|
||||
_$CreateTaskBatchFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'create_task_batch.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CreateTaskBatch _$CreateTaskBatchFromJson(Map<String, dynamic> json) =>
|
||||
CreateTaskBatch(
|
||||
reqs: (json['reqs'] as List<dynamic>?)
|
||||
?.map((e) => Request.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
opt: json['opt'] == null
|
||||
? null
|
||||
: Option.fromJson(json['opt'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
|
||||
final val = <String, dynamic>{
|
||||
'reqs': instance.reqs,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('opt', instance.opt);
|
||||
return val;
|
||||
}
|
||||
@@ -80,6 +80,7 @@ class ExtraConfig {
|
||||
String locale;
|
||||
bool lastDeleteTaskKeep;
|
||||
bool defaultDirectDownload;
|
||||
bool defaultBtClient;
|
||||
|
||||
ExtraConfigBt bt = ExtraConfigBt();
|
||||
|
||||
@@ -88,6 +89,7 @@ class ExtraConfig {
|
||||
this.locale = '',
|
||||
this.lastDeleteTaskKeep = false,
|
||||
this.defaultDirectDownload = false,
|
||||
this.defaultBtClient = true,
|
||||
});
|
||||
|
||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
|
||||
@@ -73,6 +73,7 @@ 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) =>
|
||||
@@ -81,6 +82,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
'locale': instance.locale,
|
||||
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||
'defaultBtClient': instance.defaultBtClient,
|
||||
'bt': instance.bt.toJson(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// 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,
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'info.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Info _$InfoFromJson(Map<String, dynamic> json) => Info(
|
||||
version: json['version'] as String? ?? '',
|
||||
runtime: json['runtime'] as String? ?? '',
|
||||
os: json['os'] as String? ?? '',
|
||||
arch: json['arch'] as String? ?? '',
|
||||
inDocker: json['inDocker'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InfoToJson(Info instance) => <String, dynamic>{
|
||||
'version': instance.version,
|
||||
'runtime': instance.runtime,
|
||||
'os': instance.os,
|
||||
'arch': instance.arch,
|
||||
'inDocker': instance.inDocker,
|
||||
};
|
||||
@@ -10,8 +10,8 @@ class Options {
|
||||
Object? extra;
|
||||
|
||||
Options({
|
||||
required this.name,
|
||||
required this.path,
|
||||
this.name = '',
|
||||
this.path = '',
|
||||
this.selectFiles = const [],
|
||||
this.extra,
|
||||
});
|
||||
@@ -24,12 +24,16 @@ class Options {
|
||||
|
||||
@JsonSerializable()
|
||||
class OptsExtraHttp {
|
||||
int connections = 0;
|
||||
bool autoTorrent = false;
|
||||
int connections;
|
||||
bool autoTorrent;
|
||||
|
||||
OptsExtraHttp();
|
||||
OptsExtraHttp({
|
||||
this.connections = 0,
|
||||
this.autoTorrent = false,
|
||||
});
|
||||
|
||||
factory OptsExtraHttp.fromJson(Map<String, dynamic> json) =>
|
||||
_$OptsExtraHttpFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this);
|
||||
}
|
||||
@@ -7,8 +7,8 @@ part of 'options.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
|
||||
name: json['name'] as String,
|
||||
path: json['path'] as String,
|
||||
name: json['name'] as String? ?? '',
|
||||
path: json['path'] as String? ?? '',
|
||||
selectFiles: (json['selectFiles'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList() ??
|
||||
@@ -34,9 +34,10 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
|
||||
}
|
||||
|
||||
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
|
||||
OptsExtraHttp()
|
||||
..connections = (json['connections'] as num).toInt()
|
||||
..autoTorrent = json['autoTorrent'] as bool;
|
||||
OptsExtraHttp(
|
||||
connections: (json['connections'] as num?)?.toInt() ?? 0,
|
||||
autoTorrent: json['autoTorrent'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
|
||||
<String, dynamic>{
|
||||
|
||||
@@ -6,9 +6,9 @@ part 'request.g.dart';
|
||||
class Request {
|
||||
String url;
|
||||
Object? extra;
|
||||
Map<String, String>? labels = {};
|
||||
Map<String, String>? labels;
|
||||
RequestProxy? proxy;
|
||||
bool skipVerifyCert = false;
|
||||
bool skipVerifyCert;
|
||||
|
||||
Request({
|
||||
required this.url,
|
||||
@@ -26,11 +26,15 @@ class Request {
|
||||
|
||||
@JsonSerializable()
|
||||
class ReqExtraHttp {
|
||||
String method = 'GET';
|
||||
Map<String, String> header = {};
|
||||
String body = '';
|
||||
String method;
|
||||
Map<String, String> header;
|
||||
String body;
|
||||
|
||||
ReqExtraHttp();
|
||||
ReqExtraHttp({
|
||||
this.method = 'GET',
|
||||
this.header = const {},
|
||||
this.body = '',
|
||||
});
|
||||
|
||||
factory ReqExtraHttp.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReqExtraHttpFromJson(json);
|
||||
@@ -40,9 +44,11 @@ class ReqExtraHttp {
|
||||
|
||||
@JsonSerializable()
|
||||
class ReqExtraBt {
|
||||
List<String> trackers = [];
|
||||
List<String> trackers;
|
||||
|
||||
ReqExtraBt();
|
||||
ReqExtraBt({
|
||||
this.trackers = const [],
|
||||
});
|
||||
|
||||
factory ReqExtraBt.fromJson(Map<String, dynamic> json) =>
|
||||
_$ReqExtraBtFromJson(json);
|
||||
@@ -58,15 +64,22 @@ enum RequestProxyMode {
|
||||
|
||||
@JsonSerializable()
|
||||
class RequestProxy {
|
||||
RequestProxyMode mode = RequestProxyMode.follow;
|
||||
String scheme = 'http';
|
||||
String host = '';
|
||||
String usr = '';
|
||||
String pwd = '';
|
||||
RequestProxyMode mode;
|
||||
String scheme;
|
||||
String host;
|
||||
String usr;
|
||||
String pwd;
|
||||
|
||||
RequestProxy();
|
||||
RequestProxy({
|
||||
this.mode = RequestProxyMode.follow,
|
||||
this.scheme = 'http',
|
||||
this.host = '',
|
||||
this.usr = '',
|
||||
this.pwd = '',
|
||||
});
|
||||
|
||||
factory RequestProxy.fromJson(Map<String, dynamic> json) =>
|
||||
_$RequestProxyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$RequestProxyToJson(this);
|
||||
}
|
||||
@@ -36,10 +36,14 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
return val;
|
||||
}
|
||||
|
||||
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;
|
||||
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? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -48,21 +52,26 @@ 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();
|
||||
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt(
|
||||
trackers: (json['trackers'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
|
||||
<String, dynamic>{
|
||||
'trackers': instance.trackers,
|
||||
};
|
||||
|
||||
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;
|
||||
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? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) =>
|
||||
<String, dynamic>{
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// 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',
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
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!;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
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});
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// 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,
|
||||
};
|
||||
@@ -9,72 +9,58 @@ import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_handler/share_handler.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:uri_to_file/uri_to_file.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../core/common/start_config.dart';
|
||||
import '../../../../core/libgopeed_boot.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../database/entity.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../../util/github_mirror.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/log_util.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
import '../../../../util/updater.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../create/dto/create_router_params.dart';
|
||||
import '../../redirect/views/redirect_view.dart';
|
||||
|
||||
const unixSocketPath = 'gopeed.sock';
|
||||
|
||||
const allTrackerSubscribeUrls = [
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_http.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_https.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ip.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_udp.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ws.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best.txt',
|
||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best_ip.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/all.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt',
|
||||
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_http.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_https.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_udp.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ws.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt',
|
||||
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt',
|
||||
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt',
|
||||
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt',
|
||||
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/http.txt',
|
||||
];
|
||||
const allTrackerCdns = [
|
||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
|
||||
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
[
|
||||
"https://ghproxy.com/https://raw.githubusercontent.com",
|
||||
r".*github.com(/.*)/raw(/.*)"
|
||||
],
|
||||
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
[
|
||||
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
|
||||
r".*github.com(/.*)/raw(/.*)"
|
||||
],
|
||||
];
|
||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||
key: (v) => v as String,
|
||||
value: (v) {
|
||||
final ret = [v as String];
|
||||
for (final cdn in allTrackerCdns) {
|
||||
final reg = RegExp(cdn[1]);
|
||||
final match = reg.firstMatch(v.toString());
|
||||
var matchStr = "";
|
||||
for (var i = 1; i <= match!.groupCount; i++) {
|
||||
matchStr += match.group(i)!;
|
||||
}
|
||||
ret.add("${cdn[0]}$matchStr");
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
final allTrackerSubscribeUrlCdns = {
|
||||
for (var v in allTrackerSubscribeUrls)
|
||||
v: githubMirrorUrls(v, MirrorType.githubSource)
|
||||
};
|
||||
|
||||
class AppController extends GetxController with WindowListener, TrayListener {
|
||||
static StartConfig? _defaultStartConfig;
|
||||
|
||||
final autoStartup = false.obs;
|
||||
final startConfig = StartConfig().obs;
|
||||
final runningPort = 0.obs;
|
||||
final downloaderConfig = DownloaderConfig().obs;
|
||||
final httpRunningPort = 0.obs;
|
||||
|
||||
late AppLinks _appLinks;
|
||||
StreamSubscription<Uri>? _linkSubscription;
|
||||
@@ -100,13 +86,16 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
_initLaunchAtStartup().onError((error, stackTrace) =>
|
||||
logger.w("initLaunchAtStartup error", error, stackTrace));
|
||||
|
||||
_initCheckUpdate().onError((error, stackTrace) =>
|
||||
logger.w("initCheckUpdate error", error, stackTrace));
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_linkSubscription?.cancel();
|
||||
trayManager.removeListener(this);
|
||||
LibgopeedBoot.instance.close();
|
||||
LibgopeedBoot.instance.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -155,22 +144,46 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<void> _initDeepLinks() async {
|
||||
// currently only support android
|
||||
if (!Util.isAndroid()) {
|
||||
if (Util.isWeb()) {
|
||||
return;
|
||||
}
|
||||
|
||||
_appLinks = AppLinks();
|
||||
// Handle deep link
|
||||
() async {
|
||||
_appLinks = AppLinks();
|
||||
|
||||
// Handle link when app is in warm state (front or background)
|
||||
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
|
||||
await _toCreate(uri);
|
||||
});
|
||||
// Handle link when app is in warm state (front or background)
|
||||
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
|
||||
await _handleDeepLink(uri);
|
||||
});
|
||||
|
||||
// Check initial link if app was in cold state (terminated)
|
||||
final uri = await _appLinks.getInitialAppLink();
|
||||
if (uri != null) {
|
||||
await _toCreate(uri);
|
||||
// Check initial link if app was in cold state (terminated)
|
||||
final uri = await _appLinks.getInitialLink();
|
||||
if (uri != null) {
|
||||
await _handleDeepLink(uri);
|
||||
}
|
||||
}();
|
||||
|
||||
// Handle shared media, e.g. shared link from browser
|
||||
if (Util.isMobile()) {
|
||||
() async {
|
||||
final handler = ShareHandlerPlatform.instance;
|
||||
|
||||
handler.sharedMediaStream.listen((SharedMedia media) {
|
||||
if (media.content?.isNotEmpty == true) {
|
||||
final uri = Uri.parse(media.content!);
|
||||
// content uri will be handled by the app_links plugin
|
||||
if (uri.scheme != "content") {
|
||||
_handleDeepLink(uri);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final media = await handler.getInitialSharedMedia();
|
||||
if (media?.content?.isNotEmpty == true) {
|
||||
_handleDeepLink(Uri.parse(media!.content!));
|
||||
}
|
||||
}();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,12 +226,11 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
),
|
||||
MenuItem(
|
||||
label: "startAll".tr,
|
||||
onClick: (menuItem) async =>
|
||||
{LibgopeedBoot.instance.continueTasks(null)},
|
||||
onClick: (menuItem) async => {continueAllTasks(null)},
|
||||
),
|
||||
MenuItem(
|
||||
label: "pauseAll".tr,
|
||||
onClick: (menuItem) async => {LibgopeedBoot.instance.pauseTasks(null)},
|
||||
onClick: (menuItem) async => {pauseAllTasks(null)},
|
||||
),
|
||||
MenuItem(
|
||||
label: 'setting'.tr,
|
||||
@@ -243,7 +255,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
label: 'exit'.tr,
|
||||
onClick: (menuItem) async {
|
||||
try {
|
||||
await LibgopeedBoot.instance.close();
|
||||
await LibgopeedBoot.instance.stop();
|
||||
} catch (e) {
|
||||
logger.w("libgopeed stop fail", e);
|
||||
}
|
||||
@@ -300,18 +312,80 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toCreate(Uri uri) async {
|
||||
final path = (uri.scheme == "magnet" ||
|
||||
uri.scheme == "http" ||
|
||||
uri.scheme == "https")
|
||||
? uri.toString()
|
||||
: (await toFile(uri.toString())).path;
|
||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
|
||||
Future<void> _handleDeepLink(Uri uri) async {
|
||||
if (uri.scheme == "gopeed") {
|
||||
if (uri.path == "/create") {
|
||||
final params = uri.queryParameters["params"];
|
||||
if (params?.isNotEmpty == true) {
|
||||
final safeParams = params!.replaceAll(" ", "+");
|
||||
final paramsJson =
|
||||
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
|
||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||
arguments: RedirectArgs(Routes.CREATE,
|
||||
arguments:
|
||||
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
|
||||
return;
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.CREATE);
|
||||
return;
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.HOME);
|
||||
return;
|
||||
}
|
||||
|
||||
String path;
|
||||
if (uri.scheme == "magnet" ||
|
||||
uri.scheme == "http" ||
|
||||
uri.scheme == "https") {
|
||||
path = uri.toString();
|
||||
} else if (uri.scheme == "file") {
|
||||
path =
|
||||
Util.isWindows() ? Uri.decodeFull(uri.path.substring(1)) : uri.path;
|
||||
} else {
|
||||
path = (await toFile(uri.toString())).path;
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||
arguments: RedirectArgs(Routes.CREATE,
|
||||
arguments: CreateRouterParams(req: Request(url: path))));
|
||||
}
|
||||
|
||||
String runningAddress() {
|
||||
if (startConfig.value.network == 'unix') {
|
||||
return startConfig.value.address;
|
||||
}
|
||||
return '${startConfig.value.address.split(':').first}:$runningPort';
|
||||
}
|
||||
|
||||
Future<StartConfig> _initDefaultStartConfig() async {
|
||||
if (_defaultStartConfig != null) {
|
||||
return _defaultStartConfig!;
|
||||
}
|
||||
_defaultStartConfig = StartConfig();
|
||||
if (!Util.supportUnixSocket()) {
|
||||
// not support unix socket, use tcp
|
||||
_defaultStartConfig!.network = "tcp";
|
||||
_defaultStartConfig!.address = "127.0.0.1:0";
|
||||
} else {
|
||||
_defaultStartConfig!.network = "unix";
|
||||
_defaultStartConfig!.address =
|
||||
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
|
||||
}
|
||||
_defaultStartConfig!.apiToken = '';
|
||||
return _defaultStartConfig!;
|
||||
}
|
||||
|
||||
Future<StartConfig> loadStartConfig() async {
|
||||
final defaultCfg = await _initDefaultStartConfig();
|
||||
final saveCfg = Database.instance.getStartConfig();
|
||||
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
|
||||
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
|
||||
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
|
||||
return startConfig.value;
|
||||
}
|
||||
|
||||
Future<DownloaderConfig> loadDownloaderConfig() async {
|
||||
try {
|
||||
downloaderConfig.value = await LibgopeedBoot.instance.getConfig();
|
||||
downloaderConfig.value = await getConfig();
|
||||
} catch (e) {
|
||||
logger.w("load downloader config fail", e, StackTrace.current);
|
||||
downloaderConfig.value = DownloaderConfig();
|
||||
@@ -344,7 +418,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
});
|
||||
refreshTrackers();
|
||||
|
||||
await LibgopeedBoot.instance.putConfig(downloaderConfig.value);
|
||||
await saveConfig();
|
||||
}
|
||||
|
||||
refreshTrackers() {
|
||||
@@ -373,7 +447,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<List<String>> _fetchTrackers(String subscribeUrl) async {
|
||||
final resp = await LibgopeedBoot.instance.proxyRequest(subscribeUrl);
|
||||
final resp = await proxyRequest(subscribeUrl);
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to get trackers, status code: ${resp.statusCode}');
|
||||
@@ -430,4 +504,19 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
args: ['--${Args.flagHidden}']);
|
||||
autoStartup.value = await launchAtStartup.isEnabled();
|
||||
}
|
||||
|
||||
Future<void> _initCheckUpdate() async {
|
||||
final versionInfo = await checkUpdate();
|
||||
if (versionInfo != null) {
|
||||
await showUpdateDialog(Get.context!, versionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveConfig() async {
|
||||
Database.instance.saveStartConfig(StartConfigEntity(
|
||||
network: startConfig.value.network,
|
||||
address: startConfig.value.address,
|
||||
apiToken: startConfig.value.apiToken));
|
||||
await putConfig(downloaderConfig.value);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:window_manager/window_manager.dart'; // Import the required packages
|
||||
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../theme/theme.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/util.dart'; // Import the required packages
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../controllers/app_controller.dart';
|
||||
|
||||
@@ -32,6 +34,19 @@ class AppView extends GetView<AppController> {
|
||||
],
|
||||
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
||||
getPages: AppPages.routes,
|
||||
|
||||
// Add listening to theme changes, set the title bar color according to the current system theme.
|
||||
builder: (context, child) {
|
||||
// if platform is desktop
|
||||
if (Util.isDesktop()){
|
||||
// actual brightness of the UI
|
||||
Brightness brightness = Theme.of(context).brightness;
|
||||
// Set the title bar to use the actual brightness of the UI
|
||||
windowManager.setBrightness(brightness);
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import '../../app/controllers/app_controller.dart';
|
||||
|
||||
class CreateController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
// final files = [].obs;
|
||||
final RxList fileInfos = [].obs;
|
||||
final RxList openedFolders = [].obs;
|
||||
final selectedIndexes = <int>[].obs;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
|
||||
part 'create_router_params.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class CreateRouterParams {
|
||||
Request? req;
|
||||
Options? opt;
|
||||
|
||||
CreateRouterParams({
|
||||
this.req,
|
||||
this.opt,
|
||||
});
|
||||
|
||||
factory CreateRouterParams.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
_$CreateRouterParamsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$CreateRouterParamsToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'create_router_params.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CreateRouterParams _$CreateRouterParamsFromJson(Map<String, dynamic> json) =>
|
||||
CreateRouterParams(
|
||||
req: json['req'] == null
|
||||
? null
|
||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
opt: json['opt'] == null
|
||||
? null
|
||||
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateRouterParamsToJson(CreateRouterParams instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('req', instance.req?.toJson());
|
||||
writeNotNull('opt', instance.opt?.toJson());
|
||||
return val;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:contentsize_tabbarview/contentsize_tabbarview.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,11 +9,12 @@ import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
||||
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../api/model/resolve_result.dart';
|
||||
import '../../../../api/model/task.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/message.dart';
|
||||
@@ -19,10 +22,11 @@ import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../../views/compact_checkbox.dart';
|
||||
import '../../../views/directory_selector.dart';
|
||||
import '../../../views/file_list_view.dart';
|
||||
import '../../../views/file_tree_view.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
import '../../history/views/history_view.dart';
|
||||
import '../controllers/create_controller.dart';
|
||||
import '../dto/create_router_params.dart';
|
||||
|
||||
class CreateView extends GetView<CreateController> {
|
||||
final _confirmFormKey = GlobalKey<FormState>();
|
||||
@@ -36,9 +40,20 @@ class CreateView extends GetView<CreateController> {
|
||||
final _proxyPortController = TextEditingController();
|
||||
final _proxyUsrController = TextEditingController();
|
||||
final _proxyPwdController = TextEditingController();
|
||||
final _httpUaController = TextEditingController();
|
||||
final _httpCookieController = TextEditingController();
|
||||
final _httpRefererController = TextEditingController();
|
||||
final _httpHeaderControllers = [
|
||||
(
|
||||
name: TextEditingController(text: "User-Agent"),
|
||||
value: TextEditingController()
|
||||
),
|
||||
(
|
||||
name: TextEditingController(text: "Cookie"),
|
||||
value: TextEditingController()
|
||||
),
|
||||
(
|
||||
name: TextEditingController(text: "Referer"),
|
||||
value: TextEditingController()
|
||||
),
|
||||
];
|
||||
final _btTrackerController = TextEditingController();
|
||||
|
||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||
@@ -60,32 +75,82 @@ class CreateView extends GetView<CreateController> {
|
||||
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
||||
}
|
||||
|
||||
final String? filePath = Get.rootDelegate.arguments();
|
||||
if (_urlController.text.isEmpty) {
|
||||
if (filePath?.isNotEmpty ?? false) {
|
||||
// get file path from route arguments
|
||||
_urlController.text = filePath!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
} else {
|
||||
// read clipboard
|
||||
Clipboard.getData('text/plain').then((value) {
|
||||
if (value?.text?.isNotEmpty ?? false) {
|
||||
if (_availableSchemes
|
||||
.where((e) =>
|
||||
value!.text!.startsWith(e) ||
|
||||
value.text!.startsWith(e.toUpperCase()))
|
||||
.isNotEmpty) {
|
||||
_urlController.text = value!.text!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
return;
|
||||
}
|
||||
final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
|
||||
if (routerParams?.req?.url.isNotEmpty ?? false) {
|
||||
// get url from route arguments
|
||||
final url = routerParams!.req!.url;
|
||||
_urlController.text = url;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
final protocol = parseProtocol(url);
|
||||
if (protocol != null) {
|
||||
final extraHandlers = {
|
||||
Protocol.http: () {
|
||||
final reqExtra = ReqExtraHttp.fromJson(
|
||||
jsonDecode(jsonEncode(routerParams.req!.extra)));
|
||||
_httpHeaderControllers.clear();
|
||||
reqExtra.header.forEach((key, value) {
|
||||
_httpHeaderControllers.add(
|
||||
(
|
||||
name: TextEditingController(text: key),
|
||||
value: TextEditingController(text: value),
|
||||
),
|
||||
);
|
||||
});
|
||||
_skipVerifyCertController.value = routerParams.req!.skipVerifyCert;
|
||||
},
|
||||
Protocol.bt: () {
|
||||
final reqExtra = ReqExtraBt.fromJson(
|
||||
jsonDecode(jsonEncode(routerParams.req!.extra)));
|
||||
_btTrackerController.text = reqExtra.trackers.join("\n");
|
||||
},
|
||||
};
|
||||
if (routerParams.req?.extra != null) {
|
||||
extraHandlers[protocol]?.call();
|
||||
}
|
||||
|
||||
recognizeMagnetUri(value!.text!);
|
||||
// handle options
|
||||
if (routerParams.opt != null) {
|
||||
_renameController.text = routerParams.opt!.name;
|
||||
_pathController.text = routerParams.opt!.path;
|
||||
|
||||
final optionsHandlers = {
|
||||
Protocol.http: () {
|
||||
final opt = routerParams.opt!;
|
||||
_renameController.text = opt.name;
|
||||
_pathController.text = opt.path;
|
||||
if (opt.extra != null) {
|
||||
final optsExtraHttp =
|
||||
OptsExtraHttp.fromJson(jsonDecode(jsonEncode(opt.extra)));
|
||||
_connectionsController.text =
|
||||
optsExtraHttp.connections.toString();
|
||||
}
|
||||
},
|
||||
Protocol.bt: null,
|
||||
};
|
||||
if (routerParams.opt?.extra != null) {
|
||||
optionsHandlers[protocol]?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (_urlController.text.isEmpty) {
|
||||
// read clipboard
|
||||
Clipboard.getData('text/plain').then((value) {
|
||||
if (value?.text?.isNotEmpty ?? false) {
|
||||
if (_availableSchemes
|
||||
.where((e) =>
|
||||
value!.text!.startsWith(e) ||
|
||||
value.text!.startsWith(e.toUpperCase()))
|
||||
.isNotEmpty) {
|
||||
_urlController.text = value!.text!;
|
||||
_urlController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _urlController.text.length));
|
||||
return;
|
||||
}
|
||||
|
||||
recognizeMagnetUri(value!.text!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
@@ -436,56 +501,105 @@ class CreateView extends GetView<CreateController> {
|
||||
)
|
||||
],
|
||||
),
|
||||
AutoScaleTabBarView(
|
||||
controller: controller.advancedTabController,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _httpUaController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'User-Agent',
|
||||
)),
|
||||
TextFormField(
|
||||
controller: _httpCookieController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Cookie',
|
||||
)),
|
||||
TextFormField(
|
||||
controller: _httpRefererController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Referer',
|
||||
)),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 10),
|
||||
child: CompactCheckbox(
|
||||
label: 'skipVerifyCert'.tr,
|
||||
value:
|
||||
_skipVerifyCertController.value,
|
||||
onChanged: (bool? value) {
|
||||
_skipVerifyCertController.value =
|
||||
value ?? false;
|
||||
},
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.grey,
|
||||
DefaultTabController(
|
||||
length: 2,
|
||||
child: ContentSizeTabBarView(
|
||||
controller:
|
||||
controller.advancedTabController,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
..._httpHeaderControllers.map((e) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: e.name,
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
'httpHeaderName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 10)),
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: e.value,
|
||||
decoration: InputDecoration(
|
||||
hintText:
|
||||
'httpHeaderValue'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 10)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
_httpHeaderControllers.add(
|
||||
(
|
||||
name:
|
||||
TextEditingController(),
|
||||
value:
|
||||
TextEditingController(),
|
||||
),
|
||||
);
|
||||
controller.showAdvanced
|
||||
.update((val) => val);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon:
|
||||
const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
if (_httpHeaderControllers
|
||||
.length <=
|
||||
1) {
|
||||
return;
|
||||
}
|
||||
_httpHeaderControllers
|
||||
.remove(e);
|
||||
controller.showAdvanced
|
||||
.update((val) => val);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 10),
|
||||
child: CompactCheckbox(
|
||||
label: 'skipVerifyCert'.tr,
|
||||
value: _skipVerifyCertController
|
||||
.value,
|
||||
onChanged: (bool? value) {
|
||||
_skipVerifyCertController
|
||||
.value = value ?? false;
|
||||
},
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _btTrackerController,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Trackers',
|
||||
hintText: 'addTrackerHit'.tr,
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _btTrackerController,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Trackers',
|
||||
hintText: 'addTrackerHit'.tr,
|
||||
)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
).paddingOnly(top: 16),
|
||||
@@ -549,6 +663,20 @@ class CreateView extends GetView<CreateController> {
|
||||
);
|
||||
}
|
||||
|
||||
// parse protocol from url
|
||||
parseProtocol(String url) {
|
||||
final uppercaseUrl = url.toUpperCase();
|
||||
Protocol? protocol;
|
||||
if (uppercaseUrl.startsWith("HTTP:") || uppercaseUrl.startsWith("HTTPS:")) {
|
||||
protocol = Protocol.http;
|
||||
}
|
||||
if (uppercaseUrl.startsWith("MAGNET:") ||
|
||||
uppercaseUrl.endsWith(".TORRENT")) {
|
||||
protocol = Protocol.bt;
|
||||
}
|
||||
return protocol;
|
||||
}
|
||||
|
||||
// recognize magnet uri, if length == 40, auto add magnet prefix
|
||||
recognizeMagnetUri(String text) {
|
||||
if (text.length != 40) {
|
||||
@@ -594,7 +722,7 @@ class CreateView extends GetView<CreateController> {
|
||||
final isDirect = controller.directDownload.value || isMultiLine;
|
||||
if (isDirect) {
|
||||
await Future.wait(urls.map((url) {
|
||||
return LibgopeedBoot.instance.createTask(CreateTask(
|
||||
return createTask(CreateTask(
|
||||
req: Request(
|
||||
url: url,
|
||||
extra: parseReqExtra(url),
|
||||
@@ -610,7 +738,7 @@ class CreateView extends GetView<CreateController> {
|
||||
}));
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
} else {
|
||||
final rr = await LibgopeedBoot.instance.resolve(Request(
|
||||
final rr = await resolve(Request(
|
||||
url: submitUrl,
|
||||
extra: parseReqExtra(_urlController.text),
|
||||
proxy: parseProxy(),
|
||||
@@ -641,26 +769,23 @@ class CreateView extends GetView<CreateController> {
|
||||
|
||||
Object? parseReqExtra(String url) {
|
||||
Object? reqExtra;
|
||||
if (controller.showAdvanced.value) {
|
||||
switch (controller.advancedTabController.index) {
|
||||
case 0:
|
||||
final header = {
|
||||
"User-Agent": _httpUaController.text,
|
||||
"Cookie": _httpCookieController.text,
|
||||
"Referer": _httpRefererController.text,
|
||||
};
|
||||
header.removeWhere((key, value) => value.trim().isEmpty);
|
||||
if (header.isNotEmpty) {
|
||||
reqExtra = ReqExtraHttp()..header = header;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (_btTrackerController.text.trim().isNotEmpty) {
|
||||
reqExtra = ReqExtraBt()
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
final protocol = parseProtocol(url);
|
||||
switch (protocol) {
|
||||
case Protocol.http:
|
||||
final header = Map<String, String>.fromEntries(_httpHeaderControllers
|
||||
.map((e) => MapEntry(e.name.text, e.value.text)));
|
||||
header.removeWhere(
|
||||
(key, value) => key.trim().isEmpty || value.trim().isEmpty);
|
||||
if (header.isNotEmpty) {
|
||||
reqExtra = ReqExtraHttp()..header = header;
|
||||
}
|
||||
break;
|
||||
case Protocol.bt:
|
||||
if (_btTrackerController.text.trim().isNotEmpty) {
|
||||
reqExtra = ReqExtraBt()
|
||||
..trackers = Util.textToLines(_btTrackerController.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return reqExtra;
|
||||
}
|
||||
@@ -698,7 +823,13 @@ class CreateView extends GetView<CreateController> {
|
||||
child: Form(
|
||||
key: createFormKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: FileListView(files: rr.res.files)),
|
||||
child: FileTreeView(
|
||||
files: rr.res.files,
|
||||
initialValues: rr.res.files.asMap().keys.toList(),
|
||||
onSelectionChanged: (List<int> values) {
|
||||
controller.selectedIndexes.value = values;
|
||||
},
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -741,28 +872,23 @@ class CreateView extends GetView<CreateController> {
|
||||
await Future.wait(
|
||||
controller.selectedIndexes.map((index) {
|
||||
final file = rr.res.files[index];
|
||||
return LibgopeedBoot.instance.createTask(
|
||||
CreateTask(
|
||||
req: file.req!..proxy = parseProxy(),
|
||||
opt: Options(
|
||||
name: file.name,
|
||||
path: path.join(
|
||||
_pathController.text,
|
||||
rr.res.name,
|
||||
file.path),
|
||||
selectFiles: [],
|
||||
extra: optExtra)));
|
||||
return createTask(CreateTask(
|
||||
req: file.req!..proxy = parseProxy(),
|
||||
opt: Options(
|
||||
name: file.name,
|
||||
path: path.join(_pathController.text,
|
||||
rr.res.name, file.path),
|
||||
selectFiles: [],
|
||||
extra: optExtra)));
|
||||
}));
|
||||
} else {
|
||||
await LibgopeedBoot.instance.createTask(
|
||||
CreateTask(
|
||||
rid: rr.id,
|
||||
opt: Options(
|
||||
name: _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles:
|
||||
controller.selectedIndexes,
|
||||
extra: optExtra)));
|
||||
await createTask(CreateTask(
|
||||
rid: rr.id,
|
||||
opt: Options(
|
||||
name: _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles: controller.selectedIndexes,
|
||||
extra: optExtra)));
|
||||
}
|
||||
Get.back();
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/api.dart';
|
||||
|
||||
import '../../../../api/libgopeed_boot.dart';
|
||||
import '../../../../api/model/extension.dart';
|
||||
|
||||
class ExtensionController extends GetxController {
|
||||
@@ -17,13 +17,12 @@ class ExtensionController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
extensions.value = await LibgopeedBoot.instance.getExtensions();
|
||||
extensions.value = await getExtensions();
|
||||
}
|
||||
|
||||
Future<void> checkUpdate() async {
|
||||
for (final ext in extensions) {
|
||||
final resp =
|
||||
await LibgopeedBoot.instance.upgradeCheckExtension(ext.identity);
|
||||
final resp = await upgradeCheckExtension(ext.identity);
|
||||
if (resp.newVersion.isNotEmpty) {
|
||||
updateFlags[ext.identity] = resp.newVersion;
|
||||
}
|
||||
|
||||
Loaded 100 of 185 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user