mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-11 10:54:08 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06671be0d8 | ||
|
|
8ae9363f0f | ||
|
|
5d84445f8f | ||
|
|
7c541e4047 | ||
|
|
3262e5a11b | ||
|
|
ff43a373cd | ||
|
|
7394d948e4 | ||
|
|
5afb3aa43b | ||
|
|
7705af6e68 | ||
|
|
684e060ef3 | ||
|
|
0c67792441 | ||
|
|
b0ef2c2a87 | ||
|
|
1977c2a0fc | ||
|
|
d289708f9e | ||
|
|
cd5fd45667 | ||
|
|
50bc9ab129 | ||
|
|
bfad91dcab | ||
|
|
61a959adda | ||
|
|
f4b839fd11 | ||
|
|
969aeb5e0a | ||
|
|
68d42eb397 |
No files matched your search
+43
-23
@@ -43,18 +43,26 @@ template: |
|
|||||||
<td rowspan="2">🪟 Windows</td>
|
<td rowspan="2">🪟 Windows</td>
|
||||||
<td><code>EXE</code></td>
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>Portable</code></td>
|
<td><code>Portable</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
@@ -70,32 +78,44 @@ template: |
|
|||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2"><code>DEB</code></td>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2"><code>AppImage</code></td>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<td><code>APK</code></td>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
<td><code>IPA</code></td>
|
<td><code>IPA</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🐳 Docker</td>
|
<td>🐳 Docker</td>
|
||||||
@@ -107,47 +127,47 @@ template: |
|
|||||||
<td rowspan="2">💾 Qnap</td>
|
<td rowspan="2">💾 Qnap</td>
|
||||||
<td rowspan="2"><code>QPKG</code></td>
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="8">🌐 Web</td>
|
<td rowspan="8">🌐 Web</td>
|
||||||
<td rowspan="3"><code>Windows</code></td>
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>386</td>
|
<td>386</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2"><code>MacOS</code></td>
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="3"><code>Linux</code></td>
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>386</td>
|
<td>386</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+60
-35
@@ -13,7 +13,7 @@ on:
|
|||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
FLUTTER_VERSION: "3.24.5"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -139,9 +139,9 @@ jobs:
|
|||||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
asset_path: ui/flutter/build/windows/Output/*
|
asset_path: ui/flutter/build/windows/Output/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-macos-arm64-lib:
|
build-macos-amd64-lib:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-13
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -157,15 +157,18 @@ jobs:
|
|||||||
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
|
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-amd64-lib
|
||||||
path: |
|
path: |
|
||||||
libgopeed.dylib
|
libgopeed.dylib
|
||||||
host
|
host
|
||||||
updater
|
updater
|
||||||
build-macos:
|
build-macos:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-13
|
runs-on: macos-latest
|
||||||
needs: [get-release, build-macos-arm64-lib]
|
strategy:
|
||||||
|
matrix:
|
||||||
|
channel: [ arm64, amd64, universal ]
|
||||||
|
needs: [get-release, build-macos-amd64-lib]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -183,34 +186,46 @@ jobs:
|
|||||||
npm install -g appdmg
|
npm install -g appdmg
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-amd64-lib
|
||||||
path: ui/flutter/lib-arm64
|
path: ui/flutter/lib-amd64
|
||||||
|
- if: ${{ matrix.channel == 'arm64' }}
|
||||||
|
name: Build Arm64 Libraries
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
- if: ${{ matrix.channel == 'amd64' }}
|
||||||
|
name: Build Amd64 Libraries
|
||||||
|
run: |
|
||||||
|
mkdir -p ui/flutter/macos/Frameworks
|
||||||
|
|
||||||
|
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/libgopeed.dylib
|
||||||
|
cp ui/flutter/lib-amd64/host ui/flutter/assets/exec/host
|
||||||
|
cp ui/flutter/lib-amd64/updater ui/flutter/assets/exec/updater
|
||||||
|
- if: ${{ matrix.channel == 'universal' }}
|
||||||
|
name: Build Universal Libraries
|
||||||
|
run: |
|
||||||
|
mkdir -p ui/flutter/macos/Frameworks
|
||||||
|
|
||||||
|
# arm64 lib
|
||||||
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/.temp/libgopeed.dylib.arm64 github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/.temp/host.arm64 github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/.temp/updater.arm64 github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
|
||||||
|
# amd64 lib
|
||||||
|
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.amd64
|
||||||
|
cp ui/flutter/lib-amd64/host ui/flutter/.temp/host.amd64
|
||||||
|
cp ui/flutter/lib-amd64/updater ui/flutter/.temp/updater.amd64
|
||||||
|
|
||||||
|
# universal lib
|
||||||
|
lipo -create -output ui/flutter/macos/Frameworks/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.arm64 ui/flutter/.temp/libgopeed.dylib.amd64
|
||||||
|
lipo -create -output ui/flutter/assets/exec/host ui/flutter/.temp/host.arm64 ui/flutter/.temp/host.amd64
|
||||||
|
lipo -create -output ui/flutter/assets/exec/updater ui/flutter/.temp/updater.arm64 ui/flutter/.temp/updater.amd64
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
PROJECT_DIR=$(pwd)
|
cd ui/flutter
|
||||||
|
|
||||||
# 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 --dart-define="UPDATE_CHANNEL=macosDmg"
|
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||||
cd build/macos/Build/Products/Release
|
cd build/macos/Build/Products/Release
|
||||||
cat>appdmg.json<<EOF
|
cat>appdmg.json<<EOF
|
||||||
@@ -225,6 +240,13 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
mkdir dist
|
mkdir dist
|
||||||
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
||||||
|
|
||||||
|
if [[ "${{ matrix.channel }}" == "arm64" ]]; then
|
||||||
|
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-arm64.dmg
|
||||||
|
fi
|
||||||
|
if [[ "${{ matrix.channel }}" == "amd64" ]]; then
|
||||||
|
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-amd64.dmg
|
||||||
|
fi
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -236,7 +258,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-22.04, ubuntu-22.04-arm ]
|
os: [ubuntu-22.04, ubuntu-22.04-arm]
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -270,7 +292,7 @@ jobs:
|
|||||||
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 -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/host github.com/GopeedLab/gopeed/cmd/host
|
||||||
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
cat << EOF > distribute_options.yaml
|
cat << EOF > distribute_options.yaml
|
||||||
output: dist/
|
output: dist/
|
||||||
@@ -295,13 +317,13 @@ jobs:
|
|||||||
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||||
flutter_distributor release --name linux
|
flutter_distributor release --name linux
|
||||||
cd dist/*
|
cd dist/*
|
||||||
|
|
||||||
# Get current running architecture, x86_64 -> amd64 and aarch64 -> arm64
|
# Get current running architecture, x86_64 -> amd64 and aarch64 -> arm64
|
||||||
ARCH="amd64"
|
ARCH="amd64"
|
||||||
if [[ "${{ matrix.os }}" == *-arm ]]; then
|
if [[ "${{ matrix.os }}" == *-arm ]]; then
|
||||||
ARCH="arm64"
|
ARCH="arm64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
|
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
|
||||||
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
|
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
|
||||||
- name: Upload
|
- name: Upload
|
||||||
@@ -472,8 +494,12 @@ jobs:
|
|||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
|
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
|
||||||
|
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk"
|
||||||
mkdir dist
|
mkdir dist
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
|
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
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -529,7 +555,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --no-web-resources-cdn
|
flutter build web --no-web-resources-cdn
|
||||||
cd ../../
|
cd ../../
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
FLUTTER_VERSION: "3.24.5"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Run Unit tests.
|
- name: Run Unit tests.
|
||||||
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
|
run: go test -v -coverpkg=$(go list ./... | grep -v /cmd/ | grep -v /bind/ | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /cmd/ | grep -v /bind/)
|
||||||
- uses: codecov/codecov-action@v4
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
|
|||||||
@@ -35,11 +35,19 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
@@ -70,10 +78,22 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<td><code>APK</code></td>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
|
|||||||
+24
-5
@@ -35,11 +35,19 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
@@ -70,10 +78,22 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<td><code>APK</code></td>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
@@ -135,7 +155,6 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||||
|
|
||||||
### 🛠️ コマンドツール
|
### 🛠️ コマンドツール
|
||||||
|
|||||||
+24
-5
@@ -35,11 +35,19 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
@@ -70,10 +78,22 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<td><code>APK</code></td>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
@@ -135,7 +155,6 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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)
|
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
|
||||||
|
|
||||||
### 🛠️ Công cụ lệnh
|
### 🛠️ Công cụ lệnh
|
||||||
|
|||||||
+24
-5
@@ -35,11 +35,19 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
@@ -70,10 +78,22 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<td><code>APK</code></td>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
@@ -135,7 +155,6 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
### 🛠️ 命令行工具
|
### 🛠️ 命令行工具
|
||||||
|
|||||||
+24
-4
@@ -35,11 +35,19 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
@@ -70,10 +78,22 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<td><code>APK</code></td>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ type DefaultFileController struct {
|
|||||||
|
|
||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
GetConfig: func(v any) {},
|
GetConfig: func(v any) {},
|
||||||
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) {
|
||||||
|
return requestProxy.ToHandler()
|
||||||
|
},
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type config struct {
|
|||||||
ListenPort int `json:"listenPort"`
|
ListenPort int `json:"listenPort"`
|
||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
||||||
SeedKeep bool `json:"seedKeep`
|
SeedKeep bool `json:"seedKeep"`
|
||||||
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||||
SeedRatio float64 `json:"seedRatio"`
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
// SeedTime is the time in seconds to seed after downloading is complete.
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
"github.com/anacrolix/torrent"
|
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -19,6 +12,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -100,9 +101,6 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.meta.Req = req
|
f.meta.Req = req
|
||||||
if err := f.addTorrent(req, false); err != nil {
|
if err := f.addTorrent(req, false); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -355,10 +353,14 @@ func (f *Fetcher) WaitUpload() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||||
|
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = f.initClient(); err != nil {
|
if err = f.initClient(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
schema := util.ParseSchema(req.URL)
|
schema := util.ParseSchema(req.URL)
|
||||||
|
privateTorrent := false
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(req.URL)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else {
|
} else {
|
||||||
@@ -388,34 +390,46 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
|||||||
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info, er := metaInfo.UnmarshalInfo()
|
||||||
|
if er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Private != nil && *info.Private {
|
||||||
|
privateTorrent = true
|
||||||
|
}
|
||||||
f.torrent, err = client.AddTorrent(metaInfo)
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// use map to deduplicate
|
// Do not add external tracker to a private torrent.
|
||||||
trackers := make(map[string]bool)
|
if !privateTorrent {
|
||||||
if req.Extra != nil {
|
// use map to deduplicate
|
||||||
extra := req.Extra.(*bt.ReqExtra)
|
trackers := make(map[string]bool)
|
||||||
if len(extra.Trackers) > 0 {
|
if req.Extra != nil {
|
||||||
for _, tracker := range extra.Trackers {
|
extra := req.Extra.(*bt.ReqExtra)
|
||||||
|
if len(extra.Trackers) > 0 {
|
||||||
|
for _, tracker := range extra.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(f.config.Trackers) > 0 {
|
||||||
|
for _, tracker := range f.config.Trackers {
|
||||||
trackers[tracker] = true
|
trackers[tracker] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if len(trackers) > 0 {
|
||||||
if len(f.config.Trackers) > 0 {
|
announceList := make([][]string, 0)
|
||||||
for _, tracker := range f.config.Trackers {
|
for tracker := range trackers {
|
||||||
trackers[tracker] = true
|
announceList = append(announceList, []string{tracker})
|
||||||
|
}
|
||||||
|
f.torrent.AddTrackers(announceList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(trackers) > 0 {
|
|
||||||
announceList := make([][]string, 0)
|
|
||||||
for tracker := range trackers {
|
|
||||||
announceList = append(announceList, []string{tracker})
|
|
||||||
}
|
|
||||||
f.torrent.AddTrackers(announceList)
|
|
||||||
}
|
|
||||||
<-f.torrent.GotInfo()
|
<-f.torrent.GotInfo()
|
||||||
f.torrentReady.Store(true)
|
f.torrentReady.Store(true)
|
||||||
|
|
||||||
|
|||||||
+240
-109
@@ -16,6 +16,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
@@ -26,8 +27,11 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
const connectTimeout = 15 * time.Second
|
const (
|
||||||
const readTimeout = 15 * time.Second
|
connectTimeout = 15 * time.Second
|
||||||
|
readTimeout = 15 * time.Second
|
||||||
|
helpMinSize = 1 * 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
Code int
|
Code int
|
||||||
@@ -46,11 +50,22 @@ type chunk struct {
|
|||||||
Begin int64
|
Begin int64
|
||||||
End int64
|
End int64
|
||||||
Downloaded int64
|
Downloaded int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type connection struct {
|
||||||
|
Chunk *chunk
|
||||||
|
Downloaded int64
|
||||||
|
Completed bool
|
||||||
|
|
||||||
failed bool
|
failed bool
|
||||||
retryTimes int
|
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 {
|
func newChunk(begin int64, end int64) *chunk {
|
||||||
return &chunk{
|
return &chunk{
|
||||||
Begin: begin,
|
Begin: begin,
|
||||||
@@ -63,8 +78,11 @@ type Fetcher struct {
|
|||||||
config *config
|
config *config
|
||||||
doneCh chan error
|
doneCh chan error
|
||||||
|
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
chunks []*chunk
|
connections []*connection
|
||||||
|
helpLock sync.Mutex
|
||||||
|
redirectURL string
|
||||||
|
redirectLock sync.Mutex
|
||||||
|
|
||||||
file *os.File
|
file *os.File
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -156,6 +174,12 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
} else {
|
} else {
|
||||||
file.Name = filename
|
file.Name = filename
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
substr := "attachment; filename="
|
||||||
|
index := strings.Index(contentDisposition, substr)
|
||||||
|
if index != -1 {
|
||||||
|
file.Name = contentDisposition[index+len(substr):]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// get file filePath by URL
|
// get file filePath by URL
|
||||||
@@ -217,9 +241,10 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.chunks == nil {
|
if f.connections == nil {
|
||||||
f.chunks = f.splitChunk()
|
f.connections = f.splitConnection()
|
||||||
}
|
}
|
||||||
|
f.redirectURL = ""
|
||||||
f.fetch()
|
f.fetch()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -246,16 +271,26 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Stats() any {
|
func (f *Fetcher) Stats() any {
|
||||||
// todo http download health
|
statsConnections := make([]*fhttp.StatsConnection, 0)
|
||||||
return &fhttp.Stats{}
|
for _, connection := range f.connections {
|
||||||
|
statsConnections = append(statsConnections, &fhttp.StatsConnection{
|
||||||
|
Downloaded: connection.Downloaded,
|
||||||
|
Completed: connection.Completed,
|
||||||
|
Failed: connection.failed,
|
||||||
|
RetryTimes: connection.retryTimes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &fhttp.Stats{
|
||||||
|
Connections: statsConnections,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
p := make(fetcher.Progress, 0)
|
p := make(fetcher.Progress, 0)
|
||||||
if len(f.chunks) > 0 {
|
if len(f.connections) > 0 {
|
||||||
total := int64(0)
|
total := int64(0)
|
||||||
for _, chunk := range f.chunks {
|
for _, connection := range f.connections {
|
||||||
total += chunk.Downloaded
|
total += connection.Downloaded
|
||||||
}
|
}
|
||||||
p = append(p, total)
|
p = append(p, total)
|
||||||
}
|
}
|
||||||
@@ -270,16 +305,16 @@ func (f *Fetcher) fetch() {
|
|||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
ctx, f.cancel = context.WithCancel(context.Background())
|
ctx, f.cancel = context.WithCancel(context.Background())
|
||||||
f.eg, _ = errgroup.WithContext(ctx)
|
f.eg, _ = errgroup.WithContext(ctx)
|
||||||
chunkErrs := make([]error, len(f.chunks))
|
connectionErrs := make([]error, len(f.connections))
|
||||||
for i := 0; i < len(f.chunks); i++ {
|
for i := 0; i < len(f.connections); i++ {
|
||||||
i := i
|
i := i
|
||||||
f.eg.Go(func() error {
|
f.eg.Go(func() error {
|
||||||
err := f.fetchChunk(i, ctx)
|
err := f.run(i, ctx)
|
||||||
// if canceled, fail fast
|
// if canceled, fail fast
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
chunkErrs[i] = err
|
connectionErrs[i] = err
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -291,7 +326,7 @@ func (f *Fetcher) fetch() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check all fetch results, if any error, return
|
// check all fetch results, if any error, return
|
||||||
for _, chunkErr := range chunkErrs {
|
for _, chunkErr := range connectionErrs {
|
||||||
if chunkErr != nil {
|
if chunkErr != nil {
|
||||||
err = chunkErr
|
err = chunkErr
|
||||||
break
|
break
|
||||||
@@ -307,107 +342,192 @@ func (f *Fetcher) fetch() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
func (f *Fetcher) run(index int, ctx context.Context) (err error) {
|
||||||
chunk := f.chunks[index]
|
connection := f.connections[index]
|
||||||
chunk.failed = false
|
connection.failed = false
|
||||||
chunk.retryTimes = 0
|
connection.retryTimes = 0
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = f.buildClient()
|
client = f.buildClient()
|
||||||
buf = make([]byte, 8192)
|
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 nil
|
|
||||||
}
|
|
||||||
// if all chunks failed, return
|
|
||||||
if chunk.failed {
|
|
||||||
allFailed := true
|
|
||||||
for _, c := range f.chunks {
|
|
||||||
if !c.failed {
|
|
||||||
allFailed = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allFailed {
|
|
||||||
if chunk.retryTimes >= 3 {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
chunk.retryTimes++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
downloadChunk := func(chunk *chunk) (err error) {
|
||||||
httpReq *http.Request
|
// retry until all remain chunks failed
|
||||||
resp *http.Response
|
for {
|
||||||
)
|
// if chunk is completed, return
|
||||||
httpReq, err = f.buildRequest(ctx, f.meta.Req)
|
if f.meta.Res.Range && chunk.remain() <= 0 {
|
||||||
if err != nil {
|
return nil
|
||||||
return
|
|
||||||
}
|
|
||||||
if f.meta.Res.Range {
|
|
||||||
httpReq.Header.Set(base.HttpHeaderRange,
|
|
||||||
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
|
||||||
} else {
|
|
||||||
chunk.Downloaded = 0
|
|
||||||
}
|
|
||||||
err = func() error {
|
|
||||||
resp, err = client.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
// if all chunks failed, return
|
||||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
if connection.failed {
|
||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
allFailed := true
|
||||||
return err
|
for _, c := range f.connections {
|
||||||
}
|
if c.Completed {
|
||||||
reader := NewTimeoutReader(resp.Body, readTimeout)
|
continue
|
||||||
for {
|
}
|
||||||
n, err := reader.Read(buf)
|
if !c.failed {
|
||||||
if n > 0 {
|
allFailed = false
|
||||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
break
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
chunk.Downloaded += int64(n)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if allFailed {
|
||||||
if err == io.EOF {
|
if connection.retryTimes >= 3 {
|
||||||
return nil
|
return
|
||||||
|
} else {
|
||||||
|
connection.retryTimes++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = func() error {
|
||||||
|
var (
|
||||||
|
httpReq *http.Request
|
||||||
|
resp *http.Response
|
||||||
|
)
|
||||||
|
f.redirectLock.Lock()
|
||||||
|
if f.redirectURL != "" {
|
||||||
|
f.redirectLock.Unlock()
|
||||||
|
}
|
||||||
|
err = func() (err error) {
|
||||||
|
defer func() {
|
||||||
|
if f.redirectURL == "" {
|
||||||
|
if err == nil {
|
||||||
|
f.redirectURL = resp.Request.URL.String()
|
||||||
|
}
|
||||||
|
f.redirectLock.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
httpReq, err = f.buildRequest(ctx, f.meta.Req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f.meta.Res.Range {
|
||||||
|
httpReq.Header.Set(base.HttpHeaderRange,
|
||||||
|
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
||||||
|
} else {
|
||||||
|
chunk.Downloaded = 0
|
||||||
|
}
|
||||||
|
resp, err = client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||||
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connection.failed = false
|
||||||
|
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||||
|
for {
|
||||||
|
n, err := reader.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
finished := false
|
||||||
|
if f.meta.Res.Range {
|
||||||
|
remain := chunk.remain()
|
||||||
|
// If downloaded bytes exceed the remain bytes, only write remain bytes
|
||||||
|
if remain < int64(n) {
|
||||||
|
n = int(remain)
|
||||||
|
finished = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chunk.Downloaded += int64(n)
|
||||||
|
connection.Downloaded += int64(n)
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
// If canceled, do not retry
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// retry request after 1 second
|
||||||
|
connection.failed = true
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}()
|
break
|
||||||
if err != nil {
|
}
|
||||||
// If canceled, do not retry
|
return
|
||||||
if errors.Is(err, context.Canceled) {
|
}
|
||||||
return
|
|
||||||
}
|
for {
|
||||||
// retry request after 1 second
|
if err = downloadChunk(connection.Chunk); err != nil {
|
||||||
chunk.failed = true
|
return
|
||||||
time.Sleep(time.Second)
|
}
|
||||||
|
|
||||||
|
// check this connection is completed
|
||||||
|
if !f.meta.Res.Range || !f.helpOtherConnection(connection) {
|
||||||
|
connection.Completed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) helpOtherConnection(helper *connection) bool {
|
||||||
|
f.helpLock.Lock()
|
||||||
|
defer f.helpLock.Unlock()
|
||||||
|
|
||||||
|
// find the slowest connection
|
||||||
|
var maxRemainConnection *connection
|
||||||
|
var maxRemain int64
|
||||||
|
for _, r := range f.connections {
|
||||||
|
if r == helper || r.Completed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
remain := r.Chunk.remain()
|
||||||
|
if remain > maxRemain && remain > helpMinSize {
|
||||||
|
maxRemainConnection = r
|
||||||
|
maxRemain = remain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
if maxRemainConnection == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-calculate the chunk range
|
||||||
|
helper.Chunk.Begin = maxRemainConnection.Chunk.End - maxRemainConnection.Chunk.remain()/2
|
||||||
|
helper.Chunk.End = maxRemainConnection.Chunk.End
|
||||||
|
helper.Chunk.Downloaded = 0
|
||||||
|
maxRemainConnection.Chunk.End = helper.Chunk.Begin - 1
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
||||||
url, err := url.Parse(req.URL)
|
var reqUrl string
|
||||||
if err != nil {
|
if f.redirectURL != "" {
|
||||||
return
|
reqUrl = f.redirectURL
|
||||||
|
} else {
|
||||||
|
reqUrl = req.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
method string
|
method string
|
||||||
body io.Reader
|
body io.Reader
|
||||||
)
|
)
|
||||||
|
|
||||||
headers := http.Header{}
|
headers := http.Header{}
|
||||||
if req.Extra == nil {
|
if req.Extra == nil {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
@@ -432,9 +552,9 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
httpReq, err = http.NewRequestWithContext(ctx, method, reqUrl, body)
|
||||||
} else {
|
} else {
|
||||||
httpReq, err = http.NewRequest(method, url.String(), body)
|
httpReq, err = http.NewRequest(method, reqUrl, body)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -447,30 +567,33 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
return httpReq, nil
|
return httpReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
func (f *Fetcher) splitConnection() (connections []*connection) {
|
||||||
if f.meta.Res.Range {
|
if f.meta.Res.Range {
|
||||||
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
optConnections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
||||||
// 每个连接平均需要下载的分块大小
|
// 每个连接平均需要下载的分块大小
|
||||||
chunkSize := f.meta.Res.Size / int64(connections)
|
chunkSize := f.meta.Res.Size / int64(optConnections)
|
||||||
chunks = make([]*chunk, connections)
|
connections = make([]*connection, optConnections)
|
||||||
for i := 0; i < connections; i++ {
|
for i := 0; i < optConnections; i++ {
|
||||||
var (
|
var (
|
||||||
begin = chunkSize * int64(i)
|
begin = chunkSize * int64(i)
|
||||||
end int64
|
end int64
|
||||||
)
|
)
|
||||||
if i == connections-1 {
|
if i == optConnections-1 {
|
||||||
// 最后一个分块需要保证把文件下载完
|
// 最后一个分块需要保证把文件下载完
|
||||||
end = f.meta.Res.Size - 1
|
end = f.meta.Res.Size - 1
|
||||||
} else {
|
} else {
|
||||||
end = begin + chunkSize - 1
|
end = begin + chunkSize - 1
|
||||||
}
|
}
|
||||||
chunk := newChunk(begin, end)
|
connections[i] = &connection{
|
||||||
chunks[i] = chunk
|
Chunk: newChunk(begin, end),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 只支持单连接下载
|
// 只支持单连接下载
|
||||||
chunks = make([]*chunk, 1)
|
connections = make([]*connection, 1)
|
||||||
chunks[0] = newChunk(0, 0)
|
connections[0] = &connection{
|
||||||
|
Chunk: newChunk(0, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -493,8 +616,16 @@ func (f *Fetcher) buildClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeMangledString(mangled string) string {
|
||||||
|
rawBytes := make([]byte, 0, len(mangled)*3)
|
||||||
|
for _, r := range mangled {
|
||||||
|
rawBytes = append(rawBytes, byte(r))
|
||||||
|
}
|
||||||
|
return string(rawBytes)
|
||||||
|
}
|
||||||
|
|
||||||
type fetcherData struct {
|
type fetcherData struct {
|
||||||
Chunks []*chunk
|
Connections []*connection
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherManager struct {
|
type FetcherManager struct {
|
||||||
@@ -550,7 +681,7 @@ func (fm *FetcherManager) DefaultConfig() any {
|
|||||||
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
_f := f.(*Fetcher)
|
_f := f.(*Fetcher)
|
||||||
return &fetcherData{
|
return &fetcherData{
|
||||||
Chunks: _f.chunks,
|
Connections: _f.connections,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,8 +693,8 @@ func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
fetcher.meta = meta
|
fetcher.meta = meta
|
||||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||||
if len(fd.Chunks) > 0 {
|
if len(fd.Connections) > 0 {
|
||||||
fetcher.chunks = fd.Chunks
|
fetcher.connections = fd.Connections
|
||||||
}
|
}
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, "no-encode", &base.Resource{
|
||||||
|
Size: test.BuildSize,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "测试.zip",
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
|
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Range: false,
|
Range: false,
|
||||||
@@ -176,6 +186,14 @@ func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
|
|||||||
downloadError(listener, 4, 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) {
|
func TestFetcher_DownloadResume(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -238,6 +256,33 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Stats(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
}), listener, 0, t)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fetcher.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stats := fetcher.Stats().(*http.Stats)
|
||||||
|
if len(stats.Connections) != 16 {
|
||||||
|
t.Errorf("Stats() got = %v, want %v", len(stats.Connections), 16)
|
||||||
|
}
|
||||||
|
totalDownloaded := int64(0)
|
||||||
|
for _, conn := range stats.Connections {
|
||||||
|
totalDownloaded += conn.Downloaded
|
||||||
|
}
|
||||||
|
if totalDownloaded != test.BuildSize {
|
||||||
|
t.Errorf("Stats() got = %v, want %v", totalDownloaded, test.BuildSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcherManager_ParseName(t *testing.T) {
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
u string
|
u string
|
||||||
|
|||||||
+98
-57
@@ -102,6 +102,17 @@ func StartTestCustomServer() net.Listener {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
io.Copy(writer, file)
|
io.Copy(writer, file)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/no-encode", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Disposition", "attachment; filename=测试.zip")
|
||||||
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -180,68 +191,98 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
writer.WriteHeader(403)
|
writer.WriteHeader(403)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rangeFileHandle(
|
||||||
r := request.Header.Get("Range")
|
writer,
|
||||||
if r == "" {
|
request,
|
||||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
nil,
|
||||||
writer.WriteHeader(200)
|
func(file *os.File, n int64) {
|
||||||
(writer.(http.Flusher)).Flush()
|
slowCopyN(sl, writer, file, n, delay)
|
||||||
|
},
|
||||||
file, err := os.Open(BuildFile)
|
)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
slowCopy(sl, 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)
|
|
||||||
(writer.(http.Flusher)).Flush()
|
|
||||||
|
|
||||||
file, err := os.Open(BuildFile)
|
|
||||||
if err != nil {
|
|
||||||
writer.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
file.Seek(start, 0)
|
|
||||||
slowCopyN(sl, writer, file, end-start+1, delay)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return mux
|
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
|
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
|
||||||
func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
||||||
buf := make([]byte, 32*1024)
|
buf := make([]byte, 32*1024)
|
||||||
|
|||||||
@@ -163,6 +163,16 @@ func ParseOptsExtra[E any](opts *Options) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateTaskBatch struct {
|
||||||
|
Reqs []*CreateTaskBatchItem `json:"reqs"`
|
||||||
|
Opts *Options `json:"opts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskBatchItem struct {
|
||||||
|
Req *Request `json:"req"`
|
||||||
|
Opts *Options `json:"opts"`
|
||||||
|
}
|
||||||
|
|
||||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||||
type DownloaderStoreConfig struct {
|
type DownloaderStoreConfig struct {
|
||||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||||
|
|||||||
+25
-18
@@ -148,6 +148,7 @@ func (d *Downloader) Setup() error {
|
|||||||
tasks = append(tasks[:i], tasks[i+1:]...)
|
tasks = append(tasks[:i], tasks[i+1:]...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
d.assignFetcherManager(task)
|
||||||
initTask(task)
|
initTask(task)
|
||||||
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
|
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
|
||||||
task.Status = base.DownloadStatusPause
|
task.Status = base.DownloadStatusPause
|
||||||
@@ -208,7 +209,7 @@ func (d *Downloader) Setup() error {
|
|||||||
if task.Status == base.DownloadStatusRunning {
|
if task.Status == base.DownloadStatusRunning {
|
||||||
downloadDataChanged = current != task.Progress.Downloaded
|
downloadDataChanged = current != task.Progress.Downloaded
|
||||||
task.Progress.Used = task.timer.Used()
|
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
|
task.Progress.Downloaded = current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +218,7 @@ func (d *Downloader) Setup() error {
|
|||||||
uploader := task.fetcher.(fetcher.Uploader)
|
uploader := task.fetcher.(fetcher.Uploader)
|
||||||
currentUploaded := uploader.UploadedBytes()
|
currentUploaded := uploader.UploadedBytes()
|
||||||
uploadDataChanged = currentUploaded != task.Progress.Uploaded
|
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
|
task.Progress.Uploaded = currentUploaded
|
||||||
}
|
}
|
||||||
d.emit(EventKeyProgress, task)
|
d.emit(EventKeyProgress, task)
|
||||||
@@ -358,10 +359,14 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
|
|||||||
return d.doCreate(fetcher, opts)
|
return d.doCreate(fetcher, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
|
func (d *Downloader) CreateDirectBatch(req *base.CreateTaskBatch) (taskId []string, err error) {
|
||||||
taskIds := make([]string, 0)
|
taskIds := make([]string, 0)
|
||||||
for _, req := range reqs {
|
for _, ir := range req.Reqs {
|
||||||
taskId, err := d.CreateDirect(req, opts.Clone())
|
opts := ir.Opts
|
||||||
|
if opts == nil {
|
||||||
|
opts = req.Opts
|
||||||
|
}
|
||||||
|
taskId, err := d.CreateDirect(ir.Req, opts.Clone())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -573,15 +578,19 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) deleteAll() (err error) {
|
func (d *Downloader) deleteAll() (err error) {
|
||||||
|
var deleteTasksTemp []*Task
|
||||||
func() {
|
func() {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
deleteTasksTemp = append(deleteTasksTemp, task)
|
||||||
|
}
|
||||||
d.tasks = make([]*Task, 0)
|
d.tasks = make([]*Task, 0)
|
||||||
d.waitTasks = make([]*Task, 0)
|
d.waitTasks = make([]*Task, 0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, task := range d.tasks {
|
for _, task := range deleteTasksTemp {
|
||||||
if err = d.doDelete(task, true); err != nil {
|
if err = d.doDelete(task, true); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -892,18 +901,7 @@ func (d *Downloader) restoreTask(task *Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) restoreFetcher(task *Task) error {
|
func (d *Downloader) restoreFetcher(task *Task) error {
|
||||||
var fm fetcher.FetcherManager
|
v, f := task.fetcherManager.Restore()
|
||||||
for _, f := range d.cfg.FetchManagers {
|
|
||||||
if f.Name() == task.Protocol {
|
|
||||||
fm = f
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fm == nil {
|
|
||||||
return ErrUnSupportedProtocol
|
|
||||||
}
|
|
||||||
task.fetcherManager = fm
|
|
||||||
v, f := fm.Restore()
|
|
||||||
if v != nil {
|
if v != nil {
|
||||||
err := d.storage.Pop(bucketSave, task.ID, v)
|
err := d.storage.Pop(bucketSave, task.ID, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1145,6 +1143,15 @@ func logPanic(logDir string) {
|
|||||||
debug.SetCrashOutput(f, debug.CrashOptions{})
|
debug.SetCrashOutput(f, debug.CrashOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) assignFetcherManager(task *Task) error {
|
||||||
|
fm, err := d.parseFm(task.Meta.Req.URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.fetcherManager = fm
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||||
fm, err := d.parseFm(url)
|
fm, err := d.parseFm(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -137,13 +137,15 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
fileNames := make([]string, 0)
|
fileNames := make([]string, 0)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
}
|
}
|
||||||
reqs = append(reqs, req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: req,
|
||||||
|
})
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fileNames = append(fileNames, test.DownloadName)
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
} else {
|
} else {
|
||||||
@@ -160,11 +162,14 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
_, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
|
||||||
Path: test.Dir,
|
Reqs: reqs,
|
||||||
Name: test.DownloadName,
|
Opts: &base.Options{
|
||||||
Extra: http.OptsExtra{
|
Path: test.Dir,
|
||||||
Connections: 4,
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -499,13 +504,15 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
|||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
fileNames := make([]string, 0)
|
fileNames := make([]string, 0)
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
}
|
}
|
||||||
reqs = append(reqs, req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: req,
|
||||||
|
})
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fileNames = append(fileNames, test.DownloadName)
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
} else {
|
} else {
|
||||||
@@ -522,11 +529,14 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
taskIds, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
|
||||||
Path: test.Dir,
|
Reqs: reqs,
|
||||||
Name: test.DownloadName,
|
Opts: &base.Options{
|
||||||
Extra: http.OptsExtra{
|
Path: test.Dir,
|
||||||
Connections: 4,
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+17
-6
@@ -100,16 +100,27 @@ func (t *Task) clone() *Task {
|
|||||||
return util.DeepClone(t)
|
return util.DeepClone(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
func (t *Task) updateSpeed(downloaded int64, usedTime float64) int64 {
|
||||||
speedArr = append(speedArr, downloaded)
|
return calcSpeed(&t.speedArr, downloaded, usedTime)
|
||||||
if len(speedArr) > 6 {
|
}
|
||||||
speedArr = speedArr[1:]
|
|
||||||
|
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
|
var total int64
|
||||||
for _, v := range speedArr {
|
for _, v := range *speedArr {
|
||||||
total += v
|
total += v
|
||||||
}
|
}
|
||||||
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
|
||||||
|
return int64(float64(total) / float64(len(*speedArr)) / usedTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskFilter struct {
|
type TaskFilter struct {
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ type OptsExtra struct {
|
|||||||
|
|
||||||
// Stats for download
|
// Stats for download
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
// http stats
|
Connections []*StatsConnection `json:"connections"`
|
||||||
// health indicators of http
|
}
|
||||||
|
|
||||||
|
type StatsConnection struct {
|
||||||
|
Downloaded int64 `json:"downloaded"`
|
||||||
|
Completed bool `json:"completed"`
|
||||||
|
Failed bool `json:"failed"`
|
||||||
|
RetryTimes int `json:"retryTimes"`
|
||||||
}
|
}
|
||||||
+2
-2
@@ -58,13 +58,13 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||||
var req model.CreateTaskBatch
|
var req base.CreateTaskBatch
|
||||||
if ReadJson(r, w, &req) {
|
if ReadJson(r, w, &req) {
|
||||||
if len(req.Reqs) == 0 {
|
if len(req.Reqs) == 0 {
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
taskIds, err := Downloader.CreateDirectBatch(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -7,8 +7,3 @@ type CreateTask struct {
|
|||||||
Req *base.Request `json:"req"`
|
Req *base.Request `json:"req"`
|
||||||
Opt *base.Options `json:"opt"`
|
Opt *base.Options `json:"opt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateTaskBatch struct {
|
|
||||||
Reqs []*base.Request `json:"reqs"`
|
|
||||||
Opt *base.Options `json:"opt"`
|
|
||||||
}
|
|
||||||
+81
-5
@@ -10,12 +10,14 @@ import (
|
|||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -126,16 +128,28 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
if startCfg.WebEnable {
|
if startCfg.WebEnable {
|
||||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
||||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||||
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
|
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
|
enableApiToken := startCfg.ApiToken != ""
|
||||||
|
enableBasicAuth := startCfg.WebEnable && startCfg.WebBasicAuth != nil
|
||||||
|
if enableApiToken || enableBasicAuth {
|
||||||
r.Use(func(h http.Handler) http.Handler {
|
r.Use(func(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
|
if enableApiToken {
|
||||||
h.ServeHTTP(w, r)
|
apiTokenHeader := r.Header["X-Api-Token"]
|
||||||
return
|
// 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 startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
||||||
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
@@ -222,6 +236,68 @@ func (e *extensionFileSystem) Open(name string) (http.File, error) {
|
|||||||
return os.Open(filepath.Join(extensionPath, path))
|
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 {
|
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
||||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
|||||||
+142
-4
@@ -14,7 +14,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -139,11 +141,13 @@ func TestCreateDirectTask(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateDirectTaskBatch(t *testing.T) {
|
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
reqs = append(reqs, createReq.Req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: createReq.Req,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
|
||||||
Reqs: reqs,
|
Reqs: reqs,
|
||||||
})
|
})
|
||||||
if len(taskIds) != len(reqs) {
|
if len(taskIds) != len(reqs) {
|
||||||
@@ -152,6 +156,45 @@ func TestCreateDirectTaskBatch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateDirectTaskBatchWithOpt(t *testing.T) {
|
||||||
|
doTest(func() {
|
||||||
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
item := &base.CreateTaskBatchItem{
|
||||||
|
Req: createReq.Req,
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
item.Opts = &base.Options{
|
||||||
|
Name: "spe_opt.data",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reqs = append(reqs, item)
|
||||||
|
}
|
||||||
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
|
||||||
|
Reqs: reqs,
|
||||||
|
Opts: &base.Options{
|
||||||
|
Name: "default_opt.data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if len(taskIds) != len(reqs) {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, taskId := range taskIds {
|
||||||
|
task := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||||
|
if i == 0 {
|
||||||
|
if !strings.Contains(task.Name(), "spe_opt") {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "spe_opt.data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(task.Name(), "default_opt") {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "default_opt.data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -478,6 +521,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) {
|
func TestDoProxy(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
||||||
@@ -564,15 +667,38 @@ func TestAuthorization(t *testing.T) {
|
|||||||
if status != http.StatusOK {
|
if status != http.StatusOK {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", 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()) {
|
func doTest(handler func()) {
|
||||||
|
doTest0(nil, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTest0(onStart func(cfg *model.StartConfig), handler func()) {
|
||||||
testFunc := func(storage model.Storage) {
|
testFunc := func(storage model.Storage) {
|
||||||
var cfg = &model.StartConfig{}
|
var cfg = &model.StartConfig{}
|
||||||
cfg.Init()
|
cfg.Init()
|
||||||
cfg.Storage = storage
|
cfg.Storage = storage
|
||||||
cfg.StorageDir = ".test_storage"
|
cfg.StorageDir = ".test_storage"
|
||||||
cfg.WebEnable = true
|
cfg.WebEnable = true
|
||||||
|
if onStart != nil {
|
||||||
|
onStart(cfg)
|
||||||
|
}
|
||||||
fileListener := doStart(cfg)
|
fileListener := doStart(cfg)
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := fileListener.Close(); err != nil {
|
if err := fileListener.Close(); err != nil {
|
||||||
@@ -582,6 +708,7 @@ func doTest(handler func()) {
|
|||||||
Downloader.Clear()
|
Downloader.Clear()
|
||||||
}()
|
}()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
Downloader.Pause(nil)
|
Downloader.Pause(nil)
|
||||||
Downloader.Delete(nil, true)
|
Downloader.Delete(nil, true)
|
||||||
os.RemoveAll(cfg.StorageDir)
|
os.RemoveAll(cfg.StorageDir)
|
||||||
@@ -603,6 +730,11 @@ func doStart(cfg *model.StartConfig) net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
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 doHttpRequest1(method string, path string, headers map[string]string, body any) (int, map[string]string, []byte) {
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
buf, _ := json.Marshal(body)
|
buf, _ := json.Marshal(body)
|
||||||
@@ -623,12 +755,18 @@ func doHttpRequest0(method string, path string, headers map[string]string, body
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
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)
|
respBody, err := io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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]) {
|
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
|
||||||
|
|||||||
+31
-24
@@ -3,19 +3,20 @@ import 'dart:io';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:flutter/foundation.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 '../util/util.dart';
|
||||||
import 'model/create_task.dart';
|
import 'model/create_task.dart';
|
||||||
|
import 'model/create_task_batch.dart';
|
||||||
import 'model/downloader_config.dart';
|
import 'model/downloader_config.dart';
|
||||||
|
import 'model/extension.dart';
|
||||||
|
import 'model/install_extension.dart';
|
||||||
import 'model/request.dart';
|
import 'model/request.dart';
|
||||||
import 'model/resolve_result.dart';
|
import 'model/resolve_result.dart';
|
||||||
import 'model/result.dart';
|
import 'model/result.dart';
|
||||||
|
import 'model/switch_extension.dart';
|
||||||
import 'model/task.dart';
|
import 'model/task.dart';
|
||||||
import 'model/update_extension_settings.dart';
|
|
||||||
import 'model/update_check_extension_resp.dart';
|
import 'model/update_check_extension_resp.dart';
|
||||||
|
import 'model/update_extension_settings.dart';
|
||||||
|
|
||||||
class _Client {
|
class _Client {
|
||||||
static _Client? _instance;
|
static _Client? _instance;
|
||||||
@@ -29,12 +30,12 @@ class _Client {
|
|||||||
_instance = _Client._internal();
|
_instance = _Client._internal();
|
||||||
var dio = Dio();
|
var dio = Dio();
|
||||||
final isUnixSocket = network == 'unix';
|
final isUnixSocket = network == 'unix';
|
||||||
var baseUrl = 'http://127.0.0.1';
|
var baseUrl = 'http://127.0.0.1/';
|
||||||
if (!isUnixSocket) {
|
if (!isUnixSocket) {
|
||||||
if (Util.isWeb()) {
|
if (Util.isWeb()) {
|
||||||
baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : '';
|
baseUrl = kDebugMode ? 'http://127.0.0.1:9999/' : '';
|
||||||
} else {
|
} else {
|
||||||
baseUrl = 'http://$address';
|
baseUrl = 'http://$address/';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dio.options.baseUrl = baseUrl;
|
dio.options.baseUrl = baseUrl;
|
||||||
@@ -104,16 +105,22 @@ Future<T> _parse<T>(
|
|||||||
|
|
||||||
Future<ResolveResult> resolve(Request request) async {
|
Future<ResolveResult> resolve(Request request) async {
|
||||||
return _parse<ResolveResult>(
|
return _parse<ResolveResult>(
|
||||||
() => _client.dio.post("/api/v1/resolve", data: request),
|
() => _client.dio.post("api/v1/resolve", data: request),
|
||||||
(data) => ResolveResult.fromJson(data));
|
(data) => ResolveResult.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> createTask(CreateTask createTask) async {
|
Future<String> createTask(CreateTask createTask) async {
|
||||||
return _parse<String>(
|
return _parse<String>(
|
||||||
() => _client.dio.post("/api/v1/tasks", data: createTask),
|
() => _client.dio.post("api/v1/tasks", data: createTask),
|
||||||
(data) => data as String);
|
(data) => data as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> createTaskBatch(CreateTaskBatch createTaskBatch) async {
|
||||||
|
return _parse<List<String>>(
|
||||||
|
() => _client.dio.post("api/v1/tasks/batch", data: createTaskBatch),
|
||||||
|
(data) => (data as List).map((e) => e as String).toList());
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Task>> getTasks(List<Status> statuses) async {
|
Future<List<Task>> getTasks(List<Status> statuses) async {
|
||||||
return _parse<List<Task>>(
|
return _parse<List<Task>>(
|
||||||
() => _client.dio.get(
|
() => _client.dio.get(
|
||||||
@@ -122,16 +129,16 @@ Future<List<Task>> getTasks(List<Status> statuses) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseTask(String id) async {
|
Future<void> pauseTask(String id) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/pause"), null);
|
return _parse(() => _client.dio.put("api/v1/tasks/$id/pause"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> continueTask(String id) async {
|
Future<void> continueTask(String id) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
return _parse(() => _client.dio.put("api/v1/tasks/$id/continue"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseAllTasks(List<String>? ids) async {
|
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
|
() => _client.dio.put("api/v1/tasks/pause", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
}),
|
}),
|
||||||
null);
|
null);
|
||||||
@@ -139,7 +146,7 @@ Future<void> pauseAllTasks(List<String>? ids) async {
|
|||||||
|
|
||||||
Future<void> continueAllTasks(List<String>? ids) async {
|
Future<void> continueAllTasks(List<String>? ids) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
|
() => _client.dio.put("api/v1/tasks/continue", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
}),
|
}),
|
||||||
null);
|
null);
|
||||||
@@ -147,12 +154,12 @@ Future<void> continueAllTasks(List<String>? ids) async {
|
|||||||
|
|
||||||
Future<void> deleteTask(String id, bool force) async {
|
Future<void> deleteTask(String id, bool force) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
() => _client.dio.delete("api/v1/tasks/$id?force=$force"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
|
() => _client.dio.delete("api/v1/tasks", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
"force": force,
|
"force": force,
|
||||||
}),
|
}),
|
||||||
@@ -160,29 +167,29 @@ Future<void> deleteTasks(List<String>? ids, bool force) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloaderConfig> getConfig() async {
|
Future<DownloaderConfig> getConfig() async {
|
||||||
return _parse(() => _client.dio.get("/api/v1/config"),
|
return _parse(() => _client.dio.get("api/v1/config"),
|
||||||
(data) => DownloaderConfig.fromJson(data));
|
(data) => DownloaderConfig.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> putConfig(DownloaderConfig config) async {
|
Future<void> putConfig(DownloaderConfig config) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/config", data: config), null);
|
return _parse(() => _client.dio.put("api/v1/config", data: config), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> installExtension(InstallExtension installExtension) async {
|
Future<void> installExtension(InstallExtension installExtension) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.post("/api/v1/extensions", data: installExtension),
|
() => _client.dio.post("api/v1/extensions", data: installExtension),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Extension>> getExtensions() async {
|
Future<List<Extension>> getExtensions() async {
|
||||||
return _parse<List<Extension>>(() => _client.dio.get("/api/v1/extensions"),
|
return _parse<List<Extension>>(() => _client.dio.get("api/v1/extensions"),
|
||||||
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateExtensionSettings(
|
Future<void> updateExtensionSettings(
|
||||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/extensions/$identity/settings",
|
() => _client.dio.put("api/v1/extensions/$identity/settings",
|
||||||
data: updateExtensionSettings),
|
data: updateExtensionSettings),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
@@ -191,22 +198,22 @@ Future<void> switchExtension(
|
|||||||
String identity, SwitchExtension switchExtension) async {
|
String identity, SwitchExtension switchExtension) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio
|
() => _client.dio
|
||||||
.put("/api/v1/extensions/$identity/switch", data: switchExtension),
|
.put("api/v1/extensions/$identity/switch", data: switchExtension),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteExtension(String identity) async {
|
Future<void> deleteExtension(String identity) async {
|
||||||
return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null);
|
return _parse(() => _client.dio.delete("api/v1/extensions/$identity"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
||||||
return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"),
|
return _parse(() => _client.dio.get("api/v1/extensions/$identity/update"),
|
||||||
(data) => UpdateCheckExtensionResp.fromJson(data));
|
(data) => UpdateCheckExtensionResp.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateExtension(String identity) async {
|
Future<void> updateExtension(String identity) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.post("/api/v1/extensions/$identity/update"), null);
|
() => _client.dio.post("api/v1/extensions/$identity/update"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response<String>> proxyRequest<T>(String uri,
|
Future<Response<String>> proxyRequest<T>(String uri,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'options.dart';
|
||||||
|
import 'request.dart';
|
||||||
|
|
||||||
|
part 'create_task_batch.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(explicitToJson: true)
|
||||||
|
class CreateTaskBatch {
|
||||||
|
List<CreateTaskBatchItem>? reqs;
|
||||||
|
Options? opt;
|
||||||
|
|
||||||
|
CreateTaskBatch({
|
||||||
|
this.reqs,
|
||||||
|
this.opt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateTaskBatch.fromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) =>
|
||||||
|
_$CreateTaskBatchFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateTaskBatchItem {
|
||||||
|
Request? req;
|
||||||
|
Options? opts;
|
||||||
|
|
||||||
|
CreateTaskBatchItem({
|
||||||
|
this.req,
|
||||||
|
this.opts,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateTaskBatchItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CreateTaskBatchItemFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$CreateTaskBatchItemToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// 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) => CreateTaskBatchItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
opt: json['opt'] == null
|
||||||
|
? null
|
||||||
|
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
|
||||||
|
final val = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('reqs', instance.reqs?.map((e) => e.toJson()).toList());
|
||||||
|
writeNotNull('opt', instance.opt?.toJson());
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateTaskBatchItem _$CreateTaskBatchItemFromJson(Map<String, dynamic> json) =>
|
||||||
|
CreateTaskBatchItem(
|
||||||
|
req: json['req'] == null
|
||||||
|
? null
|
||||||
|
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||||
|
opts: json['opts'] == null
|
||||||
|
? null
|
||||||
|
: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CreateTaskBatchItemToJson(CreateTaskBatchItem instance) {
|
||||||
|
final val = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('req', instance.req);
|
||||||
|
writeNotNull('opts', instance.opts);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
|||||||
|
|
||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
import '../../../../api/model/create_task.dart';
|
import '../../../../api/model/create_task.dart';
|
||||||
|
import '../../../../api/model/create_task_batch.dart';
|
||||||
import '../../../../api/model/options.dart';
|
import '../../../../api/model/options.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
import '../../../../api/model/resolve_result.dart';
|
import '../../../../api/model/resolve_result.dart';
|
||||||
@@ -869,18 +870,20 @@ class CreateView extends GetView<CreateController> {
|
|||||||
if (createFormKey.currentState!.validate()) {
|
if (createFormKey.currentState!.validate()) {
|
||||||
if (rr.id.isEmpty) {
|
if (rr.id.isEmpty) {
|
||||||
// from extension resolve result
|
// from extension resolve result
|
||||||
await Future.wait(
|
final reqs =
|
||||||
controller.selectedIndexes.map((index) {
|
controller.selectedIndexes.map((index) {
|
||||||
final file = rr.res.files[index];
|
final file = rr.res.files[index];
|
||||||
return createTask(CreateTask(
|
return CreateTaskBatchItem(
|
||||||
req: file.req!..proxy = parseProxy(),
|
req: file.req!..proxy = parseProxy(),
|
||||||
opt: Options(
|
opts: Options(
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: path.join(_pathController.text,
|
path: path.join(_pathController.text,
|
||||||
rr.res.name, file.path),
|
rr.res.name, file.path),
|
||||||
selectFiles: [],
|
selectFiles: [],
|
||||||
extra: optExtra)));
|
extra: optExtra));
|
||||||
}));
|
}).toList();
|
||||||
|
await createTaskBatch(
|
||||||
|
CreateTaskBatch(reqs: reqs));
|
||||||
} else {
|
} else {
|
||||||
await createTask(CreateTask(
|
await createTask(CreateTask(
|
||||||
rid: rr.id,
|
rid: rr.id,
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
const ptBR = {
|
||||||
|
'pt_BR': {
|
||||||
|
'label': 'Português(Brasil)',
|
||||||
|
'error': 'Erro',
|
||||||
|
'tip': 'Dica',
|
||||||
|
'confirm': 'Confirmar',
|
||||||
|
'cancel': 'Cancelar',
|
||||||
|
'on': 'Ligado',
|
||||||
|
'off': 'Desligado',
|
||||||
|
'selectAll': 'Selecionar Tudo',
|
||||||
|
'select': 'Selecionar',
|
||||||
|
'task': 'Tarefas',
|
||||||
|
'downloading': 'baixando',
|
||||||
|
'downloaded': 'baixado',
|
||||||
|
'setting': 'Configurações',
|
||||||
|
'donate': 'Doação',
|
||||||
|
'exit': 'Sair',
|
||||||
|
'create': 'Criar Tarefa',
|
||||||
|
'directDownload': 'Download Direto',
|
||||||
|
'advancedOptions': 'Opções Avançadas',
|
||||||
|
'followSettings': 'Follow Settings',
|
||||||
|
'downloadLink': 'Link de Download',
|
||||||
|
'downloadLinkValid': 'Por favor, insira o link de download',
|
||||||
|
'downloadLinkHit': 'Por favor, insira o link de download, um por linha@append',
|
||||||
|
'downloadLinkHitDesktop': ', ou arraste o arquivo torrent aqui',
|
||||||
|
'download': 'Download',
|
||||||
|
'noFileSelected': 'Por favor, selecione ao menos um arquivo para continuar',
|
||||||
|
'noStoragePermission': 'Permissão de Armazenamento Necessária',
|
||||||
|
'selectFile': 'Selecionar Arquivo',
|
||||||
|
'rename': 'Renomear para',
|
||||||
|
'basic': 'Configurações Básica',
|
||||||
|
'advanced': 'Configurações Avançada',
|
||||||
|
'general': 'Principal',
|
||||||
|
'downloadDir': 'Onde você deseja salvar?',
|
||||||
|
'downloadDirValid': 'Por favor, selecione a pasta',
|
||||||
|
'connections': 'Conexões',
|
||||||
|
'useServerCtime': 'Use server time for file creation',
|
||||||
|
'maxRunning': 'Máximo de Tarefas Simultâneas',
|
||||||
|
'defaultDirectDownload': 'Marcar download direto por padrão',
|
||||||
|
'items': '@count itens',
|
||||||
|
'subscribeTracker': 'Assinar Rastreador',
|
||||||
|
'subscribeFail':
|
||||||
|
'Falha na Assinatura, verifique sua conexão ou tente novamente mais tarde',
|
||||||
|
'update': 'Atualizar',
|
||||||
|
'updateDaily': 'Atualizar Diariamente',
|
||||||
|
'lastUpdate': 'Última Atualização: @time',
|
||||||
|
'addTracker': 'Adicionar Rastreador',
|
||||||
|
'addTrackerHit': 'Por favor, insira a URL do rastreador, um por linha',
|
||||||
|
'ui': 'Interface',
|
||||||
|
'theme': 'Tema',
|
||||||
|
'themeSystem': 'Sistema',
|
||||||
|
'themeLight': 'Claro',
|
||||||
|
'themeDark': 'Escuro',
|
||||||
|
'locale': 'Idioma',
|
||||||
|
'about': 'Sobre',
|
||||||
|
'homepage': 'Site',
|
||||||
|
'version': 'Versão',
|
||||||
|
'protocol': 'Protocolo',
|
||||||
|
'port': 'Porta',
|
||||||
|
'apiToken': 'API Token',
|
||||||
|
'notSet': 'NS',
|
||||||
|
'set': 'SET',
|
||||||
|
'portInUse': 'Porta [@port] em uso, por favor utilize outra porta',
|
||||||
|
'effectAfterRestart': 'Effect after restart',
|
||||||
|
'developer': 'Desenvolvedor',
|
||||||
|
'logDirectory': 'Pasta de Logs',
|
||||||
|
'show': 'Mostrar',
|
||||||
|
'continue': 'Continuar',
|
||||||
|
'pause': 'Pausar',
|
||||||
|
'startAll': 'Iniciar Tudo',
|
||||||
|
'pauseAll': 'Pausar Tudo',
|
||||||
|
'deleteTask': 'Excluir @count tarefas',
|
||||||
|
'deleteTaskTip': 'Manter arquivos baixados',
|
||||||
|
'delete': 'Excluir',
|
||||||
|
'newVersionTitle': 'Nova Versão Disponível @version',
|
||||||
|
'newVersionUpdate': 'Atualizar Agora',
|
||||||
|
'newVersionLater': 'Mais Tarde',
|
||||||
|
'extensions': 'Extensões',
|
||||||
|
'extensionInstallUrl': 'Link de Instalação',
|
||||||
|
'extensionInstallSuccess': 'Instalado com Sucesso',
|
||||||
|
'extensionUpdateSuccess': 'Atualizado com Sucesso',
|
||||||
|
'extensionDelete': 'Excluir Extensão',
|
||||||
|
'extensionAlreadyLatest': 'It\'s already the latest version',
|
||||||
|
'extensionFind': 'Encontrar Extensões',
|
||||||
|
'extensionDevelop': 'Desenvolvedor',
|
||||||
|
'history': 'Histórico',
|
||||||
|
'clearHistory': 'Limpar Histórico',
|
||||||
|
'noHistoryFound': 'Nenhum Histório Encontrado',
|
||||||
|
'serviceTitle': 'Download Service',
|
||||||
|
'serviceText': 'Executando',
|
||||||
|
'network': 'Rede',
|
||||||
|
'proxy': 'Proxy',
|
||||||
|
'noProxy': 'Sem Proxy',
|
||||||
|
'systemProxy': 'Proxy do Sistema',
|
||||||
|
'customProxy': 'Proxy Personalizado',
|
||||||
|
'server': 'Servidor',
|
||||||
|
'username': 'Usuário',
|
||||||
|
'password': 'Senha',
|
||||||
|
'thanks': 'Obrigado',
|
||||||
|
'thanksDesc':
|
||||||
|
'Obrigado a todos os colaboradores que ajudaram a construir e desenvolver a comunidade Gopeed!',
|
||||||
|
'browserExtension': 'Extensões para Navegadores',
|
||||||
|
'launchAtStartup': 'Launch at Startup',
|
||||||
|
'seedConfig': 'Seed Config',
|
||||||
|
'seedKeep': 'Keep seeding until manually stopped',
|
||||||
|
'seedRatio': 'Seed ratio',
|
||||||
|
'seedTime': 'Seed time (minutes)',
|
||||||
|
'setAsDefaultBtClient': 'Set as the default BT client',
|
||||||
|
'taskDetail': 'Detalhes da Tarefa',
|
||||||
|
'taskName': 'Nome da Tarefa',
|
||||||
|
'taskUrl': 'Tarefa URL',
|
||||||
|
'downloadPath': 'Download Path',
|
||||||
|
'skipVerifyCert': 'Pular Verificação de Certificado',
|
||||||
|
'name': 'Nome',
|
||||||
|
'size': 'Tamanho',
|
||||||
|
'unknown': 'Desconhecido',
|
||||||
|
'fileSelectedCount': 'Arquivos: ',
|
||||||
|
'fileSelectedSize': 'Tamanho: ',
|
||||||
|
'httpHeaderName': 'Nome do Header',
|
||||||
|
'httpHeaderValue': 'Valor do Header',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
const ruRU = {
|
const ruRU = {
|
||||||
'ru_RU': {
|
'ru_RU': {
|
||||||
'label': 'Русский(Россия)',
|
'label': 'Русский',
|
||||||
'error': 'Ошибка',
|
'error': 'Ошибка',
|
||||||
'tip': 'Подсказка',
|
'tip': 'Подсказка',
|
||||||
'confirm': 'Подтвердить',
|
'confirm': 'Подтвердить',
|
||||||
'cancel': 'Отмена',
|
'cancel': 'Отмена',
|
||||||
|
'on': 'Вкл.',
|
||||||
|
'off': 'Откл.',
|
||||||
'selectAll': 'Выбрать все',
|
'selectAll': 'Выбрать все',
|
||||||
|
'select': 'Выбрать',
|
||||||
'task': 'Задачи',
|
'task': 'Задачи',
|
||||||
'downloading': 'Загрузка',
|
'downloading': 'Загрузка',
|
||||||
'downloaded': 'Загружено',
|
'downloaded': 'Загружено',
|
||||||
@@ -13,7 +16,9 @@ const ruRU = {
|
|||||||
'donate': 'Пожертвовать',
|
'donate': 'Пожертвовать',
|
||||||
'exit': 'Выход',
|
'exit': 'Выход',
|
||||||
'create': 'Создать задачу',
|
'create': 'Создать задачу',
|
||||||
|
'directDownload': 'Прямая загрузка',
|
||||||
'advancedOptions': 'Расширенные опции',
|
'advancedOptions': 'Расширенные опции',
|
||||||
|
'followSettings': 'По умолчанию',
|
||||||
'downloadLink': 'Ссылка на скачивание',
|
'downloadLink': 'Ссылка на скачивание',
|
||||||
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
|
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
|
||||||
'downloadLinkHit':
|
'downloadLinkHit':
|
||||||
@@ -30,7 +35,9 @@ const ruRU = {
|
|||||||
'downloadDir': 'Папка загрузки',
|
'downloadDir': 'Папка загрузки',
|
||||||
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
|
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
|
||||||
'connections': 'Соединений',
|
'connections': 'Соединений',
|
||||||
|
'useServerCtime': 'Использовать время сервера для создания файла',
|
||||||
'maxRunning': 'Максимальное количество активных задач',
|
'maxRunning': 'Максимальное количество активных задач',
|
||||||
|
'defaultDirectDownload': 'Прямая загрузка по умолчанию',
|
||||||
'items': '@подсчет элементов',
|
'items': '@подсчет элементов',
|
||||||
'subscribeTracker': 'Подпись трекера',
|
'subscribeTracker': 'Подпись трекера',
|
||||||
'subscribeFail':
|
'subscribeFail':
|
||||||
@@ -40,11 +47,11 @@ const ruRU = {
|
|||||||
'lastUpdate': 'Последнее обновление: @время',
|
'lastUpdate': 'Последнее обновление: @время',
|
||||||
'addTracker': 'Добавить трекер',
|
'addTracker': 'Добавить трекер',
|
||||||
'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке',
|
'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке',
|
||||||
'ui': 'UI',
|
'ui': 'Интерфейс',
|
||||||
'theme': 'Тема',
|
'theme': 'Тема',
|
||||||
'themeSystem': 'Системная',
|
'themeSystem': 'Системная',
|
||||||
'themeLight': 'Светлая',
|
'themeLight': 'Светлая',
|
||||||
'themeDark': 'Темная',
|
'themeDark': 'Тёмная',
|
||||||
'locale': 'Язык',
|
'locale': 'Язык',
|
||||||
'about': 'О приложении',
|
'about': 'О приложении',
|
||||||
'homepage': 'Домашняя страница',
|
'homepage': 'Домашняя страница',
|
||||||
@@ -54,7 +61,13 @@ const ruRU = {
|
|||||||
'apiToken': 'API Токен',
|
'apiToken': 'API Токен',
|
||||||
'notSet': 'Не установлено',
|
'notSet': 'Не установлено',
|
||||||
'set': 'Установлено',
|
'set': 'Установлено',
|
||||||
|
'portInUse': 'Порт [@PORT] уже используется, пожалуйста, измените порт',
|
||||||
'effectAfterRestart': 'Эффект после перезагрузки',
|
'effectAfterRestart': 'Эффект после перезагрузки',
|
||||||
|
'developer': 'Разработчик',
|
||||||
|
'logDirectory': 'Каталог журналов',
|
||||||
|
'show': 'Показать',
|
||||||
|
'continue': 'Продолжить',
|
||||||
|
'pause': 'Пауза',
|
||||||
'startAll': 'Запустить все',
|
'startAll': 'Запустить все',
|
||||||
'pauseAll': 'Приостановить все',
|
'pauseAll': 'Приостановить все',
|
||||||
'deleteTask': 'Удалить @count задач',
|
'deleteTask': 'Удалить @count задач',
|
||||||
@@ -64,6 +77,7 @@ const ruRU = {
|
|||||||
'newVersionUpdate': 'Обновить',
|
'newVersionUpdate': 'Обновить',
|
||||||
'newVersionLater': 'позже',
|
'newVersionLater': 'позже',
|
||||||
'extensions': 'Расширения',
|
'extensions': 'Расширения',
|
||||||
|
'extensionInstallUrl': 'URL для установки',
|
||||||
'extensionInstallSuccess': 'Установка завершена',
|
'extensionInstallSuccess': 'Установка завершена',
|
||||||
'extensionUpdateSuccess': 'Обновление завершено',
|
'extensionUpdateSuccess': 'Обновление завершено',
|
||||||
'extensionDelete': 'Удалить расширение',
|
'extensionDelete': 'Удалить расширение',
|
||||||
@@ -73,10 +87,37 @@ const ruRU = {
|
|||||||
'history': 'История',
|
'history': 'История',
|
||||||
'clearHistory': 'Очистить историю',
|
'clearHistory': 'Очистить историю',
|
||||||
'noHistoryFound': 'История не найдена',
|
'noHistoryFound': 'История не найдена',
|
||||||
|
'serviceTitle': 'Служба загрузки',
|
||||||
|
'serviceText': 'Работает',
|
||||||
|
'network': 'Сеть',
|
||||||
|
'proxy': 'Прокси',
|
||||||
|
'noProxy': 'Без прокси',
|
||||||
|
'systemProxy': 'Системный прокси',
|
||||||
|
'customProxy': 'Пользовательский прокси',
|
||||||
|
'server': 'Сервер',
|
||||||
|
'username': 'Имя пользователя',
|
||||||
|
'password': 'Пароль',
|
||||||
'thanks': 'Благодарности',
|
'thanks': 'Благодарности',
|
||||||
'thanksDesc':
|
'thanksDesc':
|
||||||
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
|
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
|
||||||
'browserExtension': 'Расширение браузера',
|
'browserExtension': 'Расширение браузера',
|
||||||
|
'launchAtStartup': 'Запускать при загрузке системы',
|
||||||
|
'seedConfig': 'Настройка раздачи',
|
||||||
|
'seedKeep': 'Раздавать пока не остановлю вручную',
|
||||||
|
'seedRatio': 'Коэффициент раздачи',
|
||||||
|
'seedTime': 'Время раздачи (минуты)',
|
||||||
|
'setAsDefaultBtClient': 'Установить как клиент BT по умолчанию',
|
||||||
|
'taskDetail': 'Детали задания',
|
||||||
|
'taskName': 'Название задачи',
|
||||||
|
'taskUrl': 'URL задачи',
|
||||||
|
'downloadPath': 'Путь загрузки',
|
||||||
'skipVerifyCert': 'Пропустить проверку сертификата',
|
'skipVerifyCert': 'Пропустить проверку сертификата',
|
||||||
|
'name': 'Название',
|
||||||
|
'size': 'Размер',
|
||||||
|
'unknown': 'Неизвестно',
|
||||||
|
'fileSelectedCount': 'Файлов: ',
|
||||||
|
'fileSelectedSize': 'Размер: ',
|
||||||
|
'httpHeaderName': 'Название заголовка HTTP',
|
||||||
|
'httpHeaderValue': 'Значение заголовка HTTP',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -16,6 +16,7 @@ import 'langs/zh_tw.dart';
|
|||||||
import 'langs/es_es.dart';
|
import 'langs/es_es.dart';
|
||||||
import 'langs/uk_ua.dart';
|
import 'langs/uk_ua.dart';
|
||||||
import 'langs/hu_hu.dart';
|
import 'langs/hu_hu.dart';
|
||||||
|
import 'langs/pt_br.dart';
|
||||||
|
|
||||||
final messages = _Messages();
|
final messages = _Messages();
|
||||||
|
|
||||||
@@ -39,5 +40,6 @@ class _Messages extends Translations {
|
|||||||
...esES,
|
...esES,
|
||||||
...ukUA,
|
...ukUA,
|
||||||
...huHU,
|
...huHU,
|
||||||
|
...ptBR,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:open_dir/open_dir.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class FileExplorer {
|
class FileExplorer {
|
||||||
@@ -12,42 +13,19 @@ class FileExplorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _openDirectory(String directoryPath) async {
|
static Future<void> _openDirectory(String directoryPath) async {
|
||||||
await launchUrlString("file://$directoryPath");
|
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||||
|
await OpenDir().openNativeDir(path: directoryPath);
|
||||||
|
} else {
|
||||||
|
await launchUrlString("file://$directoryPath");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _openFile(String filePath) async {
|
static Future<void> _openFile(String filePath) async {
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||||
Process.run('explorer.exe', ['/select,', filePath]);
|
final fileName = path.basename(filePath);
|
||||||
} else if (Platform.isMacOS) {
|
final parentPath = path.dirname(filePath);
|
||||||
Process.run('open', ['-R', filePath]);
|
await OpenDir()
|
||||||
} else if (Platform.isLinux) {
|
.openNativeDir(path: parentPath, highlightedFileName: fileName);
|
||||||
_linuxOpen(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _linuxOpen(String filePath) async {
|
|
||||||
if (await Process.run('which', ['xdg-open'])
|
|
||||||
.then((value) => value.exitCode == 0)) {
|
|
||||||
final result = await Process.run('xdg-open', [filePath]);
|
|
||||||
if (result.exitCode != 0) {
|
|
||||||
_openWithFileManager(filePath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_openWithFileManager(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _openWithFileManager(String filePath) async {
|
|
||||||
final desktop = Platform.environment['XDG_CURRENT_DESKTOP'];
|
|
||||||
if (desktop == null) {
|
|
||||||
throw Exception('XDG_CURRENT_DESKTOP is not set');
|
|
||||||
}
|
|
||||||
if (desktop == 'GNOME') {
|
|
||||||
await Process.run('nautilus', ['--select', filePath]);
|
|
||||||
} else if (desktop == 'KDE') {
|
|
||||||
await Process.run('dolphin', ['--select', filePath]);
|
|
||||||
} else {
|
|
||||||
throw Exception('Unsupported desktop environment');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,7 +406,7 @@ String _getAssetName(String version) {
|
|||||||
case Channel.windowsPortable:
|
case Channel.windowsPortable:
|
||||||
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
|
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
|
||||||
case Channel.macosDmg:
|
case Channel.macosDmg:
|
||||||
return 'Gopeed-v$version-macos.dmg';
|
return 'Gopeed-v$version-macos-${commonArchName()}.dmg';
|
||||||
case Channel.linuxDeb:
|
case Channel.linuxDeb:
|
||||||
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
|
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
|
||||||
case Channel.androidApk:
|
case Channel.androidApk:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ endif()
|
|||||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wno-error=deprecated-declarations) # Allow deprecated warnings
|
||||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|||||||
@@ -723,6 +723,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
open_dir:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: open_dir
|
||||||
|
sha256: a4884b00e5e5795a9b4b3d582ac6a66e9196795ed760dbc3c63b4837c70c5901
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
|
open_dir_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_linux
|
||||||
|
sha256: "566cd9e02403971be06af35e1abc8057a4f3f98888c1226042e96a2af333b8bc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
|
open_dir_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_macos
|
||||||
|
sha256: "51fdc8c3a06c9d571b599b5901045ada23d1440b24c3052c0a66cf3ee4ac901b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
open_dir_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_platform_interface
|
||||||
|
sha256: ca189abb02d8e3320f9b2493b6d58e3a33f393d5eb4ccbbef02e0bc0fd393872
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
open_dir_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_windows
|
||||||
|
sha256: ec48df32ce61adb6f6cede0330d13b0d89714d2ee2df198f32ecd520e3a5d250
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
open_filex:
|
open_filex:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.6.10+1
|
version: 1.7.1+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
@@ -72,6 +72,7 @@ dependencies:
|
|||||||
win32_registry: ^1.1.5
|
win32_registry: ^1.1.5
|
||||||
share_handler: ^0.0.22
|
share_handler: ^0.0.22
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
|
open_dir: ^0.0.2+1
|
||||||
install_plugin:
|
install_plugin:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/hui-z/flutter_install_plugin.git
|
url: https://github.com/hui-z/flutter_install_plugin.git
|
||||||
|
|||||||
@@ -1,21 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!--
|
|
||||||
If you are serving your web app in a path other than the root, change the
|
|
||||||
href value below to reflect the base path you are serving from.
|
|
||||||
|
|
||||||
The path provided below has to start and end with a slash "/" in order for
|
|
||||||
it to work correctly.
|
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
||||||
|
|
||||||
This is a placeholder for base href that will be replaced by the value of
|
|
||||||
the `--base-href` argument provided to `flutter build`.
|
|
||||||
-->
|
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A modern download manager.">
|
<meta name="description" content="A modern download manager.">
|
||||||
|
|||||||
Reference in new issue
Block a user