mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 18:40:27 +00:00
Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76bec67ab2 | ||
|
|
a2a9e3dc2a | ||
|
|
9a5680fb45 | ||
|
|
75a8a3a9ac | ||
|
|
3e8c85b945 | ||
|
|
90f70d1bf6 | ||
|
|
20a70d7f91 | ||
|
|
d33546a4b5 | ||
|
|
93614d46a8 | ||
|
|
04b0e691eb | ||
|
|
7f08fa0b23 | ||
|
|
df6a51d23c | ||
|
|
46c379d556 | ||
|
|
46cd9f7a3e | ||
|
|
5c2f66f4b6 | ||
|
|
cfdcb586ee | ||
|
|
55e6bdc4f7 | ||
|
|
b31b42301e | ||
|
|
7ce0d876b0 | ||
|
|
2fd2596aa0 | ||
|
|
e0973e4a34 | ||
|
|
4f7b8a05ad | ||
|
|
2927d1ef2b | ||
|
|
3451f577ab | ||
|
|
06671be0d8 | ||
|
|
8ae9363f0f | ||
|
|
5d84445f8f | ||
|
|
7c541e4047 | ||
|
|
3262e5a11b | ||
|
|
ff43a373cd | ||
|
|
7394d948e4 | ||
|
|
5afb3aa43b | ||
|
|
7705af6e68 | ||
|
|
684e060ef3 | ||
|
|
0c67792441 | ||
|
|
b0ef2c2a87 | ||
|
|
1977c2a0fc | ||
|
|
d289708f9e | ||
|
|
cd5fd45667 | ||
|
|
50bc9ab129 | ||
|
|
bfad91dcab | ||
|
|
61a959adda | ||
|
|
f4b839fd11 | ||
|
|
969aeb5e0a | ||
|
|
68d42eb397 | ||
|
|
08603f9510 | ||
|
|
b450bf67bd | ||
|
|
03e26dbea8 | ||
|
|
046a3ac9f9 | ||
|
|
9c5aebaf36 | ||
|
|
a094715d69 | ||
|
|
69bb875172 | ||
|
|
589f10c7ef | ||
|
|
654e57e946 | ||
|
|
b94b99ae09 | ||
|
|
fba990a056 | ||
|
|
cb07ce58bc | ||
|
|
50beef7b08 | ||
|
|
bd39e4110f | ||
|
|
86efcb98c5 | ||
|
|
f3d9873395 | ||
|
|
ef228c1722 | ||
|
|
6863317e9d | ||
|
|
2b83a735cb | ||
|
|
447f599783 | ||
|
|
f986f6253d | ||
|
|
2e1b621250 | ||
|
|
d50947b2e6 | ||
|
|
520a72415c |
No files matched your search
+125
-67
@@ -38,78 +38,136 @@ version-resolver:
|
|||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Windows</th>
|
|
||||||
<th>MacOS</th>
|
|
||||||
<th colspan="4">Linux</th>
|
|
||||||
<th>Android</th>
|
|
||||||
<th>iOS</th>
|
|
||||||
<th>Docker</th>
|
|
||||||
<th colspan="2">Qnap</th>
|
|
||||||
<th colspan="8">Web</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>EXE</code></td>
|
|
||||||
<td><code>Portable</code></td>
|
|
||||||
<td><code>DMG</code></td>
|
|
||||||
<td><code>Flathub</code></td>
|
|
||||||
<td><code>SNAP</code></td>
|
|
||||||
<td><code>DEB</code></td>
|
|
||||||
<td><code>AppImage</code></td>
|
|
||||||
<td><code>APK</code></td>
|
|
||||||
<td><code>IPA</code></td>
|
|
||||||
<td>-</td>
|
|
||||||
<td colspan="2"><code>QPKG</code></td>
|
|
||||||
<td colspan="3"><code>Windows</code></td>
|
|
||||||
<td colspan="2"><code>MacOS</code></td>
|
|
||||||
<td colspan="3"><code>Linux</code></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td rowspan="2">🪟 Windows</td>
|
||||||
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>arm64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
<td><code>Portable</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Link</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Link</a></td>
|
<td>arm64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Link</a></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+113
-45
@@ -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:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-2022
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -52,10 +52,11 @@ jobs:
|
|||||||
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
|
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
|
||||||
|
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
go build -ldflags="-w -s" -o ui/flutter/assets/host/host.exe github.com/GopeedLab/gopeed/cmd/host
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host.exe github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater.exe github.com/GopeedLab/gopeed/cmd/updater
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
$TAG = "v$env:VERSION"
|
$TAG = "v$env:VERSION"
|
||||||
flutter build windows
|
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable"
|
||||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||||
$system = "C:\Windows\System32"
|
$system = "C:\Windows\System32"
|
||||||
$release = "build\windows\x64\runner\Release\"
|
$release = "build\windows\x64\runner\Release\"
|
||||||
@@ -69,6 +70,7 @@ jobs:
|
|||||||
New-Item -Path build\windows\Output -ItemType Directory
|
New-Item -Path build\windows\Output -ItemType Directory
|
||||||
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
|
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
|
||||||
|
|
||||||
|
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller"
|
||||||
cd build/windows
|
cd build/windows
|
||||||
echo @"
|
echo @"
|
||||||
; Script generated by the Inno Setup Script Wizard.
|
; Script generated by the Inno Setup Script Wizard.
|
||||||
@@ -137,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
|
||||||
@@ -152,16 +154,21 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
|
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
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
|
||||||
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
|
||||||
@@ -177,33 +184,49 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python3 -m pip install setuptools --break-system-packages
|
python3 -m pip install setuptools --break-system-packages
|
||||||
npm install -g appdmg
|
npm install -g appdmg
|
||||||
- uses: actions/download-artifact@v3
|
- 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
|
||||||
|
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||||
# 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
|
|
||||||
# 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
|
|
||||||
cd ui/flutter/macos/Frameworks
|
|
||||||
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
|
|
||||||
lipo -create -output host amd64-host arm64-host
|
|
||||||
rm -rf amd64-lib arm64-lib amd64-host arm64-host
|
|
||||||
mv host ../../assets/host/host
|
|
||||||
|
|
||||||
cd $PROJECT_DIR/ui/flutter
|
|
||||||
flutter build macos
|
|
||||||
cd build/macos/Build/Products/Release
|
cd build/macos/Build/Products/Release
|
||||||
cat>appdmg.json<<EOF
|
cat>appdmg.json<<EOF
|
||||||
{
|
{
|
||||||
@@ -217,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:
|
||||||
@@ -225,7 +255,10 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
build-linux:
|
build-linux:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, ubuntu-22.04-arm]
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -235,13 +268,15 @@ jobs:
|
|||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
channel: master
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt update -y
|
sudo apt update -y
|
||||||
# flutter build dependencies
|
# flutter build dependencies
|
||||||
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
|
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
|
||||||
# appimage build dependencies
|
# appimage build dependencies
|
||||||
sudo apt install -y libfuse2 locate
|
sudo apt install -y libfuse2 locate
|
||||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
arch=$(uname -m)
|
||||||
|
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||||
chmod +x appimagetool
|
chmod +x appimagetool
|
||||||
sudo mv appimagetool /usr/local/bin/
|
sudo mv appimagetool /usr/local/bin/
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -249,13 +284,42 @@ jobs:
|
|||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
go build -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/host/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
|
||||||
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
|
cat << EOF > distribute_options.yaml
|
||||||
|
output: dist/
|
||||||
|
releases:
|
||||||
|
- name: linux
|
||||||
|
jobs:
|
||||||
|
- name: appimage
|
||||||
|
package:
|
||||||
|
platform: linux
|
||||||
|
target: appimage
|
||||||
|
build_args:
|
||||||
|
dart-define:
|
||||||
|
UPDATE_CHANNEL: linuxAppImage
|
||||||
|
- name: deb
|
||||||
|
package:
|
||||||
|
platform: linux
|
||||||
|
target: deb
|
||||||
|
build_args:
|
||||||
|
dart-define:
|
||||||
|
UPDATE_CHANNEL: linuxDeb
|
||||||
|
EOF
|
||||||
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||||
flutter_distributor package --platform linux --targets appimage,deb
|
flutter_distributor release --name linux
|
||||||
cd dist/*
|
cd dist/*
|
||||||
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
|
|
||||||
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
|
# Get current running architecture, x86_64 -> amd64 and aarch64 -> arm64
|
||||||
|
ARCH="amd64"
|
||||||
|
if [[ "${{ matrix.os }}" == *-arm ]]; then
|
||||||
|
ARCH="arm64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
|
||||||
|
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -280,7 +344,8 @@ jobs:
|
|||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
go build -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/host/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
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
|
|
||||||
sudo snap install snapcraft --classic
|
sudo snap install snapcraft --classic
|
||||||
@@ -357,7 +422,7 @@ jobs:
|
|||||||
set +e
|
set +e
|
||||||
dart pub get
|
dart pub get
|
||||||
set -eux
|
set -eux
|
||||||
flutter build linux --release -v
|
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap"
|
||||||
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
||||||
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
||||||
gpu-2404:
|
gpu-2404:
|
||||||
@@ -422,9 +487,13 @@ jobs:
|
|||||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
flutter build apk
|
flutter build apk --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:
|
||||||
@@ -433,7 +502,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
build-ios:
|
build-ios:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-14
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -452,7 +521,7 @@ jobs:
|
|||||||
gomobile init
|
gomobile init
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build ios --no-codesign
|
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa"
|
||||||
mkdir Payload
|
mkdir Payload
|
||||||
cp -r build/ios/iphoneos/Runner.app Payload
|
cp -r build/ios/iphoneos/Runner.app Payload
|
||||||
zip -r -y Payload.zip Payload/Runner.app
|
zip -r -y Payload.zip Payload/Runner.app
|
||||||
@@ -480,7 +549,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 ../../
|
||||||
@@ -526,7 +594,7 @@ jobs:
|
|||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.8.18"
|
python-version: "3.8.18"
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: web-dist
|
name: web-dist
|
||||||
path: dist/zip/
|
path: dist/zip/
|
||||||
@@ -598,7 +666,7 @@ jobs:
|
|||||||
- name: Build flutter web
|
- name: Build flutter web
|
||||||
run: |
|
run: |
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --no-web-resources-cdn
|
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker"
|
||||||
cd ../../
|
cd ../../
|
||||||
rm -rf cmd/web/dist
|
rm -rf cmd/web/dist
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
|||||||
@@ -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,13 @@ 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: |
|
||||||
|
# Get packages for testing and coverage (including cmd/web for flags_test.go)
|
||||||
|
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
|
||||||
|
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
|
||||||
|
# Filter out main.go from coverage report to avoid lowering coverage percentage
|
||||||
|
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
|
||||||
|
mv coverage_filtered.txt coverage.txt
|
||||||
- uses: codecov/codecov-action@v4
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
@@ -63,8 +69,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest, macos-latest, ubuntu-22.04 ]
|
os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -73,6 +79,7 @@ jobs:
|
|||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
channel: master
|
||||||
- if: runner.os == 'Windows'
|
- if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
@@ -95,8 +102,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest, ubuntu-latest ]
|
os: [macos-14, ubuntu-latest]
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -128,7 +135,7 @@ jobs:
|
|||||||
build-web:
|
build-web:
|
||||||
name: Build web check
|
name: Build web check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -148,7 +155,7 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
name: Build docker check
|
name: Build docker check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -164,4 +171,4 @@ jobs:
|
|||||||
cd ../../
|
cd ../../
|
||||||
rm -rf cmd/web/dist
|
rm -rf cmd/web/dist
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
docker build -t gopeed .
|
docker build -t gopeed .
|
||||||
@@ -13,94 +13,152 @@
|
|||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## Introduction
|
## 🚀 Introduction
|
||||||
|
|
||||||
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
|
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
|
||||||
|
|
||||||
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
|
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
|
||||||
|
|
||||||
## Download
|
## ⬇️ Download
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Windows</th>
|
|
||||||
<th>MacOS</th>
|
|
||||||
<th colspan="4">Linux</th>
|
|
||||||
<th>Android</th>
|
|
||||||
<th>iOS</th>
|
|
||||||
<th>Docker</th>
|
|
||||||
<th colspan="2">Qnap</th>
|
|
||||||
<th colspan="8">Web</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>EXE</code></td>
|
|
||||||
<td><code>Portable</code></td>
|
|
||||||
<td><code>DMG</code></td>
|
|
||||||
<td><code>Flathub</code></td>
|
|
||||||
<td><code>SNAP</code></td>
|
|
||||||
<td><code>DEB</code></td>
|
|
||||||
<td><code>AppImage</code></td>
|
|
||||||
<td><code>APK</code></td>
|
|
||||||
<td><code>IPA</code></td>
|
|
||||||
<td>-</td>
|
|
||||||
<td colspan="2"><code>QPKG</code></td>
|
|
||||||
<td colspan="3"><code>Windows</code></td>
|
|
||||||
<td colspan="2"><code>MacOS</code></td>
|
|
||||||
<td colspan="3"><code>Linux</code></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td rowspan="2">🪟 Windows</td>
|
||||||
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>arm64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
<td><code>Portable</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Link</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Link</a></td>
|
<td>arm64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Link</a></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||||
|
|
||||||
### Command tool
|
### 🛠️ Command tool
|
||||||
|
|
||||||
use `go install`:
|
use `go install`:
|
||||||
|
|
||||||
@@ -108,40 +166,40 @@ use `go install`:
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Browser Extension
|
## 🔌 Browser Extension
|
||||||
|
|
||||||
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
||||||
|
|
||||||
## Donate
|
## 💝 Donate
|
||||||
|
|
||||||
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
|
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
|
||||||
|
|
||||||
## Showcase
|
## 🖼️ Showcase
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Development
|
## 👨💻 Development
|
||||||
|
|
||||||
This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
|
This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
|
||||||
|
|
||||||
> The front code is located in the `ui/flutter` directory.
|
> The front code is located in the `ui/flutter` directory.
|
||||||
|
|
||||||
### Environment
|
### 🌍 Environment
|
||||||
|
|
||||||
1. Golang 1.23+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.24+
|
2. Flutter 3.24+
|
||||||
|
|
||||||
### Clone
|
### 📋 Clone
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:GopeedLab/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### Contributing
|
### 🤝 Contributing
|
||||||
|
|
||||||
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
||||||
|
|
||||||
### Build
|
### 🏗️ Build
|
||||||
|
|
||||||
#### Desktop
|
#### Desktop
|
||||||
|
|
||||||
@@ -214,18 +272,18 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## Credits
|
## ❤️ Credits
|
||||||
|
|
||||||
### Contributors
|
### 👥 Contributors
|
||||||
|
|
||||||
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### JetBrains
|
### 🏢 JetBrains
|
||||||
|
|
||||||
[](https://www.jetbrains.com/?from=gopeed)
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
## License
|
## 📄 License
|
||||||
|
|
||||||
[GPLv3](LICENSE)
|
[GPLv3](LICENSE)
|
||||||
+137
-78
@@ -13,123 +13,182 @@
|
|||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## はじめに
|
## 🚀 はじめに
|
||||||
|
|
||||||
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
|
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
|
||||||
|
|
||||||
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
|
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
|
||||||
|
|
||||||
## インストール
|
## ⬇️ インストール
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Windows</th>
|
|
||||||
<th>MacOS</th>
|
|
||||||
<th colspan="4">Linux</th>
|
|
||||||
<th>Android</th>
|
|
||||||
<th>iOS</th>
|
|
||||||
<th>Docker</th>
|
|
||||||
<th colspan="2">Qnap</th>
|
|
||||||
<th colspan="8">Web</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>EXE</code></td>
|
|
||||||
<td><code>Portable</code></td>
|
|
||||||
<td><code>DMG</code></td>
|
|
||||||
<td><code>Flathub</code></td>
|
|
||||||
<td><code>SNAP</code></td>
|
|
||||||
<td><code>DEB</code></td>
|
|
||||||
<td><code>AppImage</code></td>
|
|
||||||
<td><code>APK</code></td>
|
|
||||||
<td><code>IPA</code></td>
|
|
||||||
<td>-</td>
|
|
||||||
<td colspan="2"><code>QPKG</code></td>
|
|
||||||
<td colspan="3"><code>Windows</code></td>
|
|
||||||
<td colspan="2"><code>MacOS</code></td>
|
|
||||||
<td colspan="3"><code>Linux</code></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td rowspan="2">🪟 Windows</td>
|
||||||
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>arm64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
<td><code>Portable</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Link</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Link</a></td>
|
<td>arm64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Link</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Link</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Link</a></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||||
|
|
||||||
## 寄付
|
### 🛠️ コマンドツール
|
||||||
|
|
||||||
|
## 💝 寄付
|
||||||
|
|
||||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||||
|
|
||||||
## ショーケース
|
## 🖼️ ショーケース
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 開発
|
## 👨💻 開発
|
||||||
|
|
||||||
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
|
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
|
||||||
|
|
||||||
> フロントコードは `ui/flutter` ディレクトリにあります。
|
> フロントコードは `ui/flutter` ディレクトリにあります。
|
||||||
|
|
||||||
### 環境
|
### 🌍 環境
|
||||||
|
|
||||||
1. Go 言語 1.23+
|
1. Go 言語 1.23+
|
||||||
2. Flutter 3.24+
|
2. Flutter 3.24+
|
||||||
|
|
||||||
### クローン
|
### 📋 クローン
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:GopeedLab/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### コントリビュート
|
### 🤝 コントリビュート
|
||||||
|
|
||||||
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
|
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
|
||||||
|
|
||||||
### ビルド
|
### 🏗️ ビルド
|
||||||
|
|
||||||
#### デスクトップ
|
#### デスクトップ
|
||||||
|
|
||||||
@@ -202,7 +261,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感謝
|
## ❤️ 感謝
|
||||||
|
|
||||||
### コントリビューター
|
### コントリビューター
|
||||||
|
|
||||||
|
|||||||
+136
-79
@@ -13,94 +13,151 @@
|
|||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## Giới thiệu
|
## 🚀 Giới thiệu
|
||||||
|
|
||||||
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
|
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
|
||||||
|
|
||||||
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
|
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
|
||||||
|
|
||||||
## Tải về
|
## ⬇️ Tải về
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Windows</th>
|
|
||||||
<th>MacOS</th>
|
|
||||||
<th colspan="4">Linux</th>
|
|
||||||
<th>Android</th>
|
|
||||||
<th>iOS</th>
|
|
||||||
<th>Docker</th>
|
|
||||||
<th colspan="2">Qnap</th>
|
|
||||||
<th colspan="8">Web</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>EXE</code></td>
|
|
||||||
<td><code>Portable</code></td>
|
|
||||||
<td><code>DMG</code></td>
|
|
||||||
<td><code>Flathub</code></td>
|
|
||||||
<td><code>SNAP</code></td>
|
|
||||||
<td><code>DEB</code></td>
|
|
||||||
<td><code>AppImage</code></td>
|
|
||||||
<td><code>APK</code></td>
|
|
||||||
<td><code>IPA</code></td>
|
|
||||||
<td>-</td>
|
|
||||||
<td colspan="2"><code>QPKG</code></td>
|
|
||||||
<td colspan="3"><code>Windows</code></td>
|
|
||||||
<td colspan="2"><code>MacOS</code></td>
|
|
||||||
<td colspan="3"><code>Linux</code></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td rowspan="2">🪟 Windows</td>
|
||||||
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>arm64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Liên kết</a></td>
|
<td><code>Portable</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Liên kết</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Liên kết</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Liên kết</a></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">Liên kết</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Liên kết</a></td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Liên kết</a></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Liên kết</a></td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Liên kết</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">Liên kết</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">Liên kết</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">Liên kết</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">Liên kết</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">Liên kết</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">Liên kết</a></td>
|
<td>arm64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">Liên kết</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">Liên kết</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">Liên kết</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">Liên kết</a></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</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)
|
||||||
|
|
||||||
### Command tool
|
### 🛠️ Công cụ lệnh
|
||||||
|
|
||||||
Sử dụng `go install`:
|
Sử dụng `go install`:
|
||||||
|
|
||||||
@@ -108,36 +165,36 @@ Sử dụng `go install`:
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quyên góp
|
## 💝 Quyên góp
|
||||||
|
|
||||||
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
||||||
|
|
||||||
## Trưng bày
|
## 🖼️ Trưng bày
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Development
|
## 👨💻 Development
|
||||||
|
|
||||||
Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`.
|
Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`.
|
||||||
|
|
||||||
> Mã giao diện nằm trong thư mục `ui/flutter`.
|
> Mã giao diện nằm trong thư mục `ui/flutter`.
|
||||||
|
|
||||||
### Environment
|
### 🌍 Environment
|
||||||
|
|
||||||
1. Golang 1.23+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.24+
|
2. Flutter 3.24+
|
||||||
|
|
||||||
### Clone
|
### 📋 Clone
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:GopeedLab/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### Đóng góp
|
### 🤝 Đóng góp
|
||||||
|
|
||||||
Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md)
|
Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md)
|
||||||
|
|
||||||
### Xây dựng
|
### 🏗️ Xây dựng
|
||||||
|
|
||||||
#### Desktop
|
#### Desktop
|
||||||
|
|
||||||
@@ -210,7 +267,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tín dụng
|
## ❤️ Tín dụng
|
||||||
|
|
||||||
### Người đóng góp
|
### Người đóng góp
|
||||||
|
|
||||||
|
|||||||
+137
-80
@@ -13,94 +13,151 @@
|
|||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## 介绍
|
## 🚀 介绍
|
||||||
|
|
||||||
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
|
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
|
||||||
|
|
||||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
|
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
|
||||||
|
|
||||||
## 下载
|
## ⬇️ 下载
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Windows</th>
|
|
||||||
<th>MacOS</th>
|
|
||||||
<th colspan="4">Linux</th>
|
|
||||||
<th>Android</th>
|
|
||||||
<th>iOS</th>
|
|
||||||
<th>Docker</th>
|
|
||||||
<th colspan="2">Qnap</th>
|
|
||||||
<th colspan="8">Web</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>EXE</code></td>
|
|
||||||
<td><code>Portable</code></td>
|
|
||||||
<td><code>DMG</code></td>
|
|
||||||
<td><code>Flathub</code></td>
|
|
||||||
<td><code>SNAP</code></td>
|
|
||||||
<td><code>DEB</code></td>
|
|
||||||
<td><code>AppImage</code></td>
|
|
||||||
<td><code>APK</code></td>
|
|
||||||
<td><code>IPA</code></td>
|
|
||||||
<td>-</td>
|
|
||||||
<td colspan="2"><code>QPKG</code></td>
|
|
||||||
<td colspan="3"><code>Windows</code></td>
|
|
||||||
<td colspan="2"><code>MacOS</code></td>
|
|
||||||
<td colspan="3"><code>Linux</code></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td rowspan="2">🪟 Windows</td>
|
||||||
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>arm64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
<td><code>Portable</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
<td>amd64</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-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">前往</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">前往</a></td>
|
<td>arm64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">前往</a></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
### 命令行工具
|
### 🛠️ 命令行工具
|
||||||
|
|
||||||
使用`go install`安装:
|
使用`go install`安装:
|
||||||
|
|
||||||
@@ -108,40 +165,40 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## 浏览器扩展
|
## 🔌 浏览器扩展
|
||||||
|
|
||||||
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
||||||
|
|
||||||
## 赞助
|
## 💝 赞助
|
||||||
|
|
||||||
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
||||||
|
|
||||||
## 界面展示
|
## 🖼️ 界面展示
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 开发
|
## 👨💻 开发
|
||||||
|
|
||||||
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
|
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
|
||||||
|
|
||||||
> 前端代码位于`ui/flutter`目录下。
|
> 前端代码位于`ui/flutter`目录下。
|
||||||
|
|
||||||
### 环境要求
|
### 🌍 环境要求
|
||||||
|
|
||||||
1. Golang 1.23+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.24+
|
2. Flutter 3.24+
|
||||||
|
|
||||||
### 克隆项目
|
### 📋 克隆项目
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:GopeedLab/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### 贡献代码
|
### 🤝 贡献代码
|
||||||
|
|
||||||
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
|
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
|
||||||
|
|
||||||
### 编译
|
### 🏗️ 编译
|
||||||
|
|
||||||
#### 桌面端
|
#### 桌面端
|
||||||
|
|
||||||
@@ -214,7 +271,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感谢
|
## ❤️ 感谢
|
||||||
|
|
||||||
### 贡献者
|
### 贡献者
|
||||||
|
|
||||||
|
|||||||
+136
-78
@@ -13,94 +13,152 @@
|
|||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## 簡介
|
## 🚀 簡介
|
||||||
|
|
||||||
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
|
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
|
||||||
|
|
||||||
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
||||||
|
|
||||||
## 下載
|
## ⬇️ 下載
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2">Windows</th>
|
|
||||||
<th>MacOS</th>
|
|
||||||
<th colspan="4">Linux</th>
|
|
||||||
<th>Android</th>
|
|
||||||
<th>iOS</th>
|
|
||||||
<th>Docker</th>
|
|
||||||
<th colspan="2">Qnap</th>
|
|
||||||
<th colspan="8">Web</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>EXE</code></td>
|
|
||||||
<td><code>Portable</code></td>
|
|
||||||
<td><code>DMG</code></td>
|
|
||||||
<td><code>Flathub</code></td>
|
|
||||||
<td><code>SNAP</code></td>
|
|
||||||
<td><code>DEB</code></td>
|
|
||||||
<td><code>AppImage</code></td>
|
|
||||||
<td><code>APK</code></td>
|
|
||||||
<td><code>IPA</code></td>
|
|
||||||
<td>-</td>
|
|
||||||
<td colspan="2"><code>QPKG</code></td>
|
|
||||||
<td colspan="3"><code>Windows</code></td>
|
|
||||||
<td colspan="2"><code>MacOS</code></td>
|
|
||||||
<td colspan="3"><code>Linux</code></td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td rowspan="2">🪟 Windows</td>
|
||||||
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>-</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>arm64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>amd64</td>
|
|
||||||
<td>386</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
<td><code>Portable</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
<td>amd64</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-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">前往</a></td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">前往</a></td>
|
<td>arm64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">前往</a></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">前往</a></td>
|
<tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">前往</a></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
### 使用 CLI 安裝
|
### 🛠️ 使用 CLI 安裝
|
||||||
|
|
||||||
使用`go install`安裝:
|
使用`go install`安裝:
|
||||||
|
|
||||||
@@ -108,36 +166,36 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## 贊助
|
## 💝 贊助
|
||||||
|
|
||||||
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||||
|
|
||||||
## 軟體介面
|
## 🖼️ 軟體介面
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 開發
|
## 👨💻 開發
|
||||||
|
|
||||||
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
|
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
|
||||||
|
|
||||||
> 前端代碼位於`ui/flutter`目錄內。
|
> 前端代碼位於`ui/flutter`目錄內。
|
||||||
|
|
||||||
### 開發環境
|
### 🌍 開發環境
|
||||||
|
|
||||||
1. Golang 1.23+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.24+
|
2. Flutter 3.24+
|
||||||
|
|
||||||
### 克隆項目
|
### 📋 克隆項目
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:GopeedLab/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### 協助開發
|
### 🤝 協助開發
|
||||||
|
|
||||||
請參考[協助指南](CONTRIBUTING_zh-TW.md)
|
請參考[協助指南](CONTRIBUTING_zh-TW.md)
|
||||||
|
|
||||||
### 編譯
|
### 🏗️ 編譯
|
||||||
|
|
||||||
#### 桌面端
|
#### 桌面端
|
||||||
|
|
||||||
@@ -210,7 +268,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感謝
|
## ❤️ 感謝
|
||||||
|
|
||||||
### 貢獻者
|
### 貢獻者
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dial() (net.Conn, error) {
|
||||||
|
// Get binary path
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return net.Dial("unix", filepath.Join(filepath.Dir(exe), "gopeed_host.sock"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dial() (net.Conn, error) {
|
||||||
|
return winio.DialPipe(`\\.\pipe\gopeed_host`, nil)
|
||||||
|
}
|
||||||
+71
-24
@@ -1,22 +1,24 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/browser"
|
"github.com/pkg/browser"
|
||||||
"github.com/shirou/gopsutil/v4/process"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const identifier = "gopeed"
|
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
|
Meta map[string]any `json:"meta"`
|
||||||
Params json.RawMessage `json:"params"`
|
Params json.RawMessage `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,35 +28,80 @@ type Response struct {
|
|||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
|
func check() (data bool, err error) {
|
||||||
"ping": func(params json.RawMessage) (data any, err error) {
|
conn, err := Dial()
|
||||||
processes, err := process.Processes()
|
if err != nil {
|
||||||
if err != nil {
|
return false, err
|
||||||
return false, err
|
}
|
||||||
}
|
defer conn.Close()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range processes {
|
func wakeup(hidden bool) error {
|
||||||
name, err := p.Name()
|
running, _ := check()
|
||||||
if err != nil {
|
if running {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
|
uri := "gopeed:"
|
||||||
return true, nil
|
if hidden {
|
||||||
}
|
uri = uri + "?hidden=true"
|
||||||
|
}
|
||||||
|
if err := browser.OpenURL(uri); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if ok, _ := check(); ok {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return false, nil
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("start gopeed failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiMap = map[string]func(message *Message) (data any, err error){
|
||||||
|
"ping": func(message *Message) (data any, err error) {
|
||||||
|
return check()
|
||||||
},
|
},
|
||||||
"create": func(params json.RawMessage) (data any, err error) {
|
"create": func(message *Message) (data any, err error) {
|
||||||
var strParams string
|
buf, err := message.Params.MarshalJSON()
|
||||||
if err = json.Unmarshal(params, &strParams); err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
|
|
||||||
|
silent := false
|
||||||
|
if v, ok := message.Meta["silent"]; ok {
|
||||||
|
silent, _ = v.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wakeup(silent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return Dial()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", "http://127.0.0.1/create", bytes.NewBuffer(buf))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if message.Meta != nil {
|
||||||
|
metaJson, _ := json.Marshal(message.Meta)
|
||||||
|
req.Header.Set("X-Gopeed-Host-Meta", string(metaJson))
|
||||||
|
}
|
||||||
|
_, err = client.Do(req)
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
for {
|
for {
|
||||||
// Read message length (first 4 bytes)
|
// Read message length (first 4 bytes)
|
||||||
@@ -86,7 +133,7 @@ func main() {
|
|||||||
var data any
|
var data any
|
||||||
var err error
|
var err error
|
||||||
if handler, ok := apiMap[message.Method]; ok {
|
if handler, ok := apiMap[message.Method]; ok {
|
||||||
data, err = handler(message.Params)
|
data, err = handler(&message)
|
||||||
} else {
|
} else {
|
||||||
err = errors.New("Unknown method: " + message.Method)
|
err = errors.New("Unknown method: " + message.Method)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/browser"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pid := flag.Int("pid", 0, "PID of the process to update")
|
||||||
|
updateChannel := flag.String("channel", "", "Update channel")
|
||||||
|
packagePath := flag.String("asset", "", "Path to the package asset")
|
||||||
|
exeDir := flag.String("exeDir", "", "Directory of the entry executable")
|
||||||
|
logPath := flag.String("log", "", "Log file path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *pid == 0 {
|
||||||
|
log.Println("Invalid PID")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if *updateChannel == "" {
|
||||||
|
log.Println("Invalid update channel")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *logPath != "" {
|
||||||
|
logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||||
|
if err == nil {
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
restart bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if restart, err = update(*pid, *updateChannel, *packagePath, *exeDir); err != nil {
|
||||||
|
log.Printf("Update failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the application
|
||||||
|
if restart {
|
||||||
|
browser.OpenURL("gopeed:///")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete package asset
|
||||||
|
if *packagePath != "" {
|
||||||
|
os.Remove(*packagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, err error) {
|
||||||
|
killSignalChan := make(chan any, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-killSignalChan
|
||||||
|
|
||||||
|
if err = killProcess(pid); err != nil {
|
||||||
|
log.Printf("Failed to kill process: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = waitForProcessExit(pid); err != nil {
|
||||||
|
log.Printf("Failed to wait for process exit: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("Updating process updateChannel=%s packagePath=%s exeDir=%s\n", updateChannel, packagePath, exeDir)
|
||||||
|
if restart, err = install(killSignalChan, updateChannel, packagePath, exeDir); err != nil {
|
||||||
|
return false, errors.Wrap(err, "failed to install package")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForProcessExit(pid int) error {
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
// On some systems, error is returned if process doesn't exist
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send null signal to test if process exists
|
||||||
|
err = process.Signal(syscall.Signal(0))
|
||||||
|
if err != nil {
|
||||||
|
// If error occurs, the process no longer exists
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("process %d still running after timeout", pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func killProcess(pid int) error {
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.Kill()
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
return true, installByDmg(killSignalChan, packagePath, destDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByDmg handles macOS dmg package installation
|
||||||
|
func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
// /Applications/Gopeed.app/Contents/MacOS -> /Applications
|
||||||
|
appPath := getParentDir(getParentDir(getParentDir(destDir)))
|
||||||
|
output, err := exec.Command("hdiutil", "attach", packagePath, "-nobrowse").Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mountPoint := ""
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
if strings.Contains(line, "/Volumes/") {
|
||||||
|
// Find the /Volumes/ path in the line
|
||||||
|
// hdiutil output format: /dev/disk4s1 Apple_HFS /Volumes/Gopeed
|
||||||
|
// or with sequence number: /dev/disk4s1 Apple_HFS /Volumes/Gopeed 1
|
||||||
|
idx := strings.Index(line, "/Volumes/")
|
||||||
|
if idx != -1 {
|
||||||
|
// Extract everything from /Volumes/ onwards and trim whitespace
|
||||||
|
mountPoint = strings.TrimSpace(line[idx:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mountPoint == "" {
|
||||||
|
return fmt.Errorf("failed to get mount point from hdiutil output: %s", string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detach the mounted DMG
|
||||||
|
defer exec.Command("hdiutil", "detach", mountPoint, "-quiet").Run()
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(filepath.Join(mountPoint, "*.app"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return fmt.Errorf("no .app found in dmg, mountPoint: %s", mountPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
killSignalChan <- nil
|
||||||
|
|
||||||
|
// Copy the new app to the destination
|
||||||
|
// cp -Rf /Volumes/GoPeed/GoPeed.app /Applications
|
||||||
|
if err := exec.Command("cp", "-Rf", matches[0], appPath).Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent directory safely, handling trailing separators
|
||||||
|
func getParentDir(path string) string {
|
||||||
|
// Remove trailing separators if they exist
|
||||||
|
path = strings.TrimRight(path, string(filepath.Separator))
|
||||||
|
// Now get the parent directory
|
||||||
|
return filepath.Dir(path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
killSignalChan <- nil
|
||||||
|
switch updateChannel {
|
||||||
|
case "linuxDeb":
|
||||||
|
return true, installByDeb(packagePath)
|
||||||
|
case "linuxFlathub":
|
||||||
|
return true, installByFlathub()
|
||||||
|
case "linuxSnap":
|
||||||
|
return true, installBySnap()
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("unsupported update channel for Linux: %s", updateChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeInTerminal tries to execute a command in one of several common terminal emulators
|
||||||
|
func executeInTerminal(command string) error {
|
||||||
|
terminals := []string{
|
||||||
|
"gnome-terminal", // GNOME
|
||||||
|
"konsole", // KDE
|
||||||
|
"xfce4-terminal", // XFCE
|
||||||
|
"xterm", // X11
|
||||||
|
}
|
||||||
|
|
||||||
|
command = fmt.Sprintf(`echo "Starting update..." && echo "[CMD] %s" && %s`, command, command)
|
||||||
|
|
||||||
|
for _, term := range terminals {
|
||||||
|
if _, err := exec.LookPath(term); err == nil {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch term {
|
||||||
|
case "gnome-terminal", "xfce4-terminal":
|
||||||
|
cmd = exec.Command(term, "--", "bash", "-c", command)
|
||||||
|
case "konsole":
|
||||||
|
cmd = exec.Command(term, "-e", "bash", "-c", command)
|
||||||
|
case "xterm":
|
||||||
|
cmd = exec.Command(term, "-e", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no suitable terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal or xterm")
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByDeb installs the .deb package
|
||||||
|
func installByDeb(packagePath string) error {
|
||||||
|
command := fmt.Sprintf(`sudo dpkg -i "%s"`, packagePath)
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByFlathub updates the application via Flathub
|
||||||
|
func installByFlathub() error {
|
||||||
|
command := "flatpak update com.gopeed.Gopeed -y"
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installBySnap updates the application via Snap
|
||||||
|
func installBySnap() error {
|
||||||
|
command := "sudo snap refresh gopeed"
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
switch updateChannel {
|
||||||
|
case "windowsInstaller":
|
||||||
|
return false, installByInstaller(killSignalChan, packagePath, destDir)
|
||||||
|
default:
|
||||||
|
return true, installByPortable(killSignalChan, packagePath, destDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByInstaller extracts the installer from the zip file and runs it
|
||||||
|
func installByInstaller(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
// Create a temp directory for extraction
|
||||||
|
tempDir, err := os.MkdirTemp("", "gopeed_update")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Extract the zip file
|
||||||
|
reader, err := zip.OpenReader(packagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
// Find the installer file
|
||||||
|
var installerPath string
|
||||||
|
for _, file := range reader.File {
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file
|
||||||
|
path := filepath.Join(tempDir, file.Name)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
dstFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
srcFile.Close()
|
||||||
|
dstFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is likely an installer (.exe, .msi), save its path
|
||||||
|
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||||
|
if ext == ".exe" || ext == ".msi" {
|
||||||
|
installerPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if installerPath == "" {
|
||||||
|
return fmt.Errorf("no installer found in the update package")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the installer
|
||||||
|
cmd := exec.Command(installerPath)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
killSignalChan <- nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByPortable extracts the portable version to the destination directory
|
||||||
|
func installByPortable(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
killSignalChan <- nil
|
||||||
|
|
||||||
|
reader, err := zip.OpenReader(packagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
|
path := filepath.Join(destDir, file.Name)
|
||||||
|
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
os.MkdirAll(path, file.Mode())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
dstFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
srcFile.Close()
|
||||||
|
dstFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+166
-51
@@ -3,19 +3,23 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
Address *string `json:"address"`
|
Address *string `json:"address"`
|
||||||
Port *int `json:"port"`
|
Port *int `json:"port"`
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
ApiToken *string `json:"apiToken"`
|
ApiToken *string `json:"apiToken"`
|
||||||
StorageDir *string `json:"storageDir"`
|
StorageDir *string `json:"storageDir"`
|
||||||
|
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||||
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
@@ -23,62 +27,173 @@ type args struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parse() *args {
|
func parse() *args {
|
||||||
var cliArgs args
|
cfg := &args{}
|
||||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
|
||||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
cliConfig := loadCliArgs()
|
||||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
loadConfigFile(cfg, *cliConfig.configPath)
|
||||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
loadEnvVars(cfg)
|
||||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
|
// override with non-default command line arguments
|
||||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
overrideWithCliArgs(cfg, cliConfig)
|
||||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
// set default values
|
||||||
|
setDefaults(cfg, cliConfig)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadCliArgs parses command line arguments and returns initial config
|
||||||
|
func loadCliArgs() *args {
|
||||||
|
cfg := &args{}
|
||||||
|
cfg.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||||
|
cfg.Port = flag.Int("P", 9999, "Bind Port")
|
||||||
|
cfg.Username = flag.String("u", "gopeed", "Web Authentication Username")
|
||||||
|
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||||
|
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||||
|
cfg.StorageDir = flag.String("d", "", "Storage directory")
|
||||||
|
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
|
||||||
|
cfg.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// args priority: config file > cli args
|
// Parse white download directories from comma-separated string
|
||||||
cfgArgs := loadConfig(*cliArgs.configPath)
|
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
|
||||||
if cfgArgs.Address == nil {
|
dirs := strings.Split(*whiteDownloadDirs, ",")
|
||||||
cfgArgs.Address = cliArgs.Address
|
for i := range dirs {
|
||||||
|
dirs[i] = strings.TrimSpace(dirs[i])
|
||||||
|
}
|
||||||
|
cfg.WhiteDownloadDirs = dirs
|
||||||
}
|
}
|
||||||
if cfgArgs.Port == nil {
|
|
||||||
cfgArgs.Port = cliArgs.Port
|
return cfg
|
||||||
}
|
|
||||||
if cfgArgs.Username == nil {
|
|
||||||
cfgArgs.Username = cliArgs.Username
|
|
||||||
}
|
|
||||||
if cfgArgs.Password == nil {
|
|
||||||
cfgArgs.Password = cliArgs.Password
|
|
||||||
}
|
|
||||||
if cfgArgs.ApiToken == nil {
|
|
||||||
cfgArgs.ApiToken = cliArgs.ApiToken
|
|
||||||
}
|
|
||||||
if cfgArgs.StorageDir == nil {
|
|
||||||
cfgArgs.StorageDir = cliArgs.StorageDir
|
|
||||||
}
|
|
||||||
return cfgArgs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(path string) *args {
|
// overrideWithCliArgs overrides config with non-empty command line arguments
|
||||||
var args args
|
func overrideWithCliArgs(cfg *args, cliConfig *args) {
|
||||||
|
flag.Visit(func(f *flag.Flag) {
|
||||||
|
switch f.Name {
|
||||||
|
case "A":
|
||||||
|
cfg.Address = cliConfig.Address
|
||||||
|
case "P":
|
||||||
|
cfg.Port = cliConfig.Port
|
||||||
|
case "u":
|
||||||
|
cfg.Username = cliConfig.Username
|
||||||
|
case "p":
|
||||||
|
cfg.Password = cliConfig.Password
|
||||||
|
case "T":
|
||||||
|
cfg.ApiToken = cliConfig.ApiToken
|
||||||
|
case "d":
|
||||||
|
cfg.StorageDir = cliConfig.StorageDir
|
||||||
|
case "w":
|
||||||
|
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
|
||||||
|
case "c":
|
||||||
|
cfg.configPath = cliConfig.configPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if !filepath.IsAbs(path) {
|
// setDefaults sets default values for any unset configuration fields
|
||||||
|
func setDefaults(cfg *args, cliConfig *args) {
|
||||||
|
if cfg.Address == nil {
|
||||||
|
cfg.Address = cliConfig.Address
|
||||||
|
}
|
||||||
|
if cfg.Port == nil {
|
||||||
|
cfg.Port = cliConfig.Port
|
||||||
|
}
|
||||||
|
if cfg.Username == nil {
|
||||||
|
cfg.Username = cliConfig.Username
|
||||||
|
}
|
||||||
|
if cfg.Password == nil {
|
||||||
|
cfg.Password = cliConfig.Password
|
||||||
|
}
|
||||||
|
if cfg.ApiToken == nil {
|
||||||
|
cfg.ApiToken = cliConfig.ApiToken
|
||||||
|
}
|
||||||
|
if cfg.StorageDir == nil {
|
||||||
|
cfg.StorageDir = cliConfig.StorageDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfigFile loads configuration from file
|
||||||
|
func loadConfigFile(cfg *args, configPath string) {
|
||||||
|
if !filepath.IsAbs(configPath) {
|
||||||
dir, err := os.Getwd()
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
return
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
path = filepath.Join(dir, path)
|
configPath = filepath.Join(dir, configPath)
|
||||||
}
|
}
|
||||||
file, err := os.ReadFile(path)
|
|
||||||
|
file, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return &args
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("config file read failed, reason:" + err.Error())
|
return
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(file, &args); err != nil {
|
|
||||||
fmt.Println("config file parse failed, reason:" + err.Error())
|
if err = json.Unmarshal(file, cfg); err != nil {
|
||||||
return &args
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
|
||||||
|
func loadEnvVars(cfg *args) {
|
||||||
|
v := reflect.ValueOf(cfg).Elem()
|
||||||
|
t := reflect.TypeOf(cfg).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := v.Field(i)
|
||||||
|
fieldType := t.Field(i)
|
||||||
|
|
||||||
|
// Get json tag as environment variable suffix
|
||||||
|
jsonTag := fieldType.Tag.Get("json")
|
||||||
|
if jsonTag == "" || jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove options like omitempty
|
||||||
|
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
|
||||||
|
jsonTag = jsonTag[:commaIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to uppercase and add GOPEED_ prefix
|
||||||
|
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
|
||||||
|
envValue := os.Getenv(envKey)
|
||||||
|
|
||||||
|
if envValue == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set value based on field type
|
||||||
|
if field.Kind() == reflect.Ptr {
|
||||||
|
if field.IsNil() {
|
||||||
|
// Create new pointer instance
|
||||||
|
newVal := reflect.New(field.Type().Elem())
|
||||||
|
field.Set(newVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Type().Elem().Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
field.Elem().SetString(envValue)
|
||||||
|
case reflect.Int:
|
||||||
|
if intVal, err := strconv.Atoi(envValue); err == nil {
|
||||||
|
field.Elem().SetInt(int64(intVal))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// For complex types like DownloadConfig, try JSON unmarshaling
|
||||||
|
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
|
||||||
|
var config base.DownloaderStoreConfig
|
||||||
|
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
|
||||||
|
field.Set(reflect.ValueOf(&config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if field.Kind() == reflect.Slice {
|
||||||
|
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
|
||||||
|
if field.Type().Elem().Kind() == reflect.String {
|
||||||
|
dirs := strings.Split(envValue, ",")
|
||||||
|
for i := range dirs {
|
||||||
|
dirs[i] = strings.TrimSpace(dirs[i])
|
||||||
|
}
|
||||||
|
field.Set(reflect.ValueOf(dirs))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("config file loaded: %s\n", path)
|
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+19
-17
@@ -6,11 +6,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/cmd"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
@@ -23,36 +24,37 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := parse()
|
args := parse()
|
||||||
var webBasicAuth *model.WebBasicAuth
|
var webAuth *model.WebAuth
|
||||||
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
||||||
webBasicAuth = &model.WebBasicAuth{
|
webAuth = &model.WebAuth{
|
||||||
Username: *args.Username,
|
Username: *args.Username,
|
||||||
Password: *args.Password,
|
Password: *args.Password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dir string
|
var storageDir string
|
||||||
if args.StorageDir != nil && *args.StorageDir != "" {
|
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||||
dir = *args.StorageDir
|
storageDir = *args.StorageDir
|
||||||
} else {
|
} else {
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
dir = filepath.Dir(exe)
|
storageDir = filepath.Join(filepath.Dir(exe), "storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &model.StartConfig{
|
cfg := &model.StartConfig{
|
||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
StorageDir: filepath.Join(dir, "storage"),
|
StorageDir: storageDir,
|
||||||
ApiToken: *args.ApiToken,
|
WhiteDownloadDirs: args.WhiteDownloadDirs,
|
||||||
DownloadConfig: args.DownloadConfig,
|
ApiToken: *args.ApiToken,
|
||||||
ProductionMode: true,
|
DownloadConfig: args.DownloadConfig,
|
||||||
WebEnable: true,
|
ProductionMode: true,
|
||||||
WebFS: sub,
|
WebEnable: true,
|
||||||
WebBasicAuth: webBasicAuth,
|
WebFS: sub,
|
||||||
|
WebAuth: webAuth,
|
||||||
}
|
}
|
||||||
cmd.Start(cfg)
|
cmd.Start(cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ require (
|
|||||||
github.com/go-git/go-git/v5 v5.8.1
|
github.com/go-git/go-git/v5 v5.8.1
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/imroc/req/v3 v3.52.2
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
github.com/mattn/go-ieproxy v0.0.12
|
github.com/mattn/go-ieproxy v0.0.12
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@@ -21,17 +22,29 @@ require (
|
|||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.3.11
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/sync v0.9.0
|
golang.org/x/sync v0.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/ebitengine/purego v0.8.1 // indirect
|
github.com/ebitengine/purego v0.8.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/icholy/digest v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.51.0 // indirect
|
||||||
|
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -61,7 +74,7 @@ require (
|
|||||||
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -77,7 +90,7 @@ require (
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
@@ -122,13 +135,13 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
golang.org/x/crypto v0.29.0 // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/net v0.31.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.20.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.27.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
modernc.org/libc v1.61.0 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
|
|||||||
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
||||||
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
||||||
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
|
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
|
||||||
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
@@ -122,8 +124,9 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M
|
|||||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -196,6 +199,8 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
@@ -229,12 +234,12 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -250,6 +255,11 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
@@ -262,6 +272,10 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
|||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
|
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||||
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
|
github.com/imroc/req/v3 v3.52.2 h1:xJocr1aIv0a2K9knfBQ4JnZHk+kWTITdjf0mgDg229I=
|
||||||
|
github.com/imroc/req/v3 v3.52.2/go.mod h1:dBGsDloOSZJcFs6PnTjZXYBJK70OXbZpizHBLNqcH2k=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
@@ -277,6 +291,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
|||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
@@ -331,7 +347,11 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
|
|||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
@@ -380,6 +400,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
@@ -404,7 +426,13 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
|||||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
||||||
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
|
||||||
|
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||||
|
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
@@ -452,8 +480,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
||||||
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
@@ -471,6 +500,8 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
|
|||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -487,8 +518,12 @@ go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZk
|
|||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||||
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||||
|
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -497,8 +532,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||||
@@ -512,8 +547,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
|||||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -535,8 +570,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -550,8 +585,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -590,17 +625,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -609,8 +642,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -624,8 +657,8 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -646,6 +679,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
|||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -667,6 +702,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ package bt
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/test"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
|
||||||
gohttp "net/http"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ package bt
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/anacrolix/torrent/storage"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
|
||||||
"github.com/anacrolix/missinggo/v2"
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
|
||||||
"github.com/anacrolix/torrent/common"
|
"github.com/anacrolix/torrent/common"
|
||||||
@@ -34,7 +35,7 @@ func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
|
|||||||
if opts.FilePathMaker == nil {
|
if opts.FilePathMaker == nil {
|
||||||
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
if opts.Info.Name != metainfo.NoName {
|
if opts.Info.Length == 0 || opts.File.Path == nil {
|
||||||
parts = append(parts, opts.Info.Name)
|
parts = append(parts, opts.Info.Name)
|
||||||
}
|
}
|
||||||
return filepath.Join(append(parts, opts.File.Path...)...)
|
return filepath.Join(append(parts, opts.File.Path...)...)
|
||||||
|
|||||||
+258
-112
@@ -16,18 +16,23 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/xiaoqidun/setft"
|
"github.com/xiaoqidun/setft"
|
||||||
"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,10 +51,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
|
||||||
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,
|
||||||
@@ -62,8 +79,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
|
||||||
@@ -147,12 +167,29 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
_, params, _ := mime.ParseMediaType(contentDisposition)
|
_, params, _ := mime.ParseMediaType(contentDisposition)
|
||||||
filename := params["filename"]
|
filename := params["filename"]
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
file.Name = filename
|
// Check if the filename is MIME encoded-word
|
||||||
|
if strings.HasPrefix(filename, "=?") {
|
||||||
|
decoder := new(mime.WordDecoder)
|
||||||
|
filename = strings.Replace(filename, "UTF8", "UTF-8", 1)
|
||||||
|
file.Name, _ = decoder.Decode(filename)
|
||||||
|
} else {
|
||||||
|
file.Name = util.TryUrlQueryUnescape(filename)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
substr := "attachment; filename="
|
||||||
|
index := strings.Index(contentDisposition, substr)
|
||||||
|
if index != -1 {
|
||||||
|
file.Name = util.TryUrlQueryUnescape(contentDisposition[index+len(substr):])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// get file filePath by URL
|
// get file filePath by URL
|
||||||
if file.Name == "" {
|
if file.Name == "" {
|
||||||
file.Name = path.Base(httpReq.URL.Path)
|
file.Name = path.Base(httpReq.URL.Path)
|
||||||
|
// Url decode
|
||||||
|
if file.Name != "" {
|
||||||
|
file.Name, _ = url.QueryUnescape(file.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// unknown file filePath
|
// unknown file filePath
|
||||||
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
||||||
@@ -205,9 +242,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
|
||||||
}
|
}
|
||||||
@@ -234,16 +272,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)
|
||||||
}
|
}
|
||||||
@@ -254,24 +302,20 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
return <-f.doneCh
|
return <-f.doneCh
|
||||||
}
|
}
|
||||||
|
|
||||||
type fetchResult struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) fetch() {
|
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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -283,7 +327,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
|
||||||
@@ -299,102 +343,193 @@ 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.retryTimes = 0
|
connection.failed = false
|
||||||
|
connection.retryTimes = 0
|
||||||
var (
|
var (
|
||||||
client = f.buildClient()
|
client = f.buildClient()
|
||||||
buf = make([]byte, 8192)
|
buf = make([]byte, 8192)
|
||||||
maxRetries = 3
|
|
||||||
)
|
)
|
||||||
// retry until all remain chunks failed
|
|
||||||
for {
|
|
||||||
// if chunk is completed, return
|
|
||||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if chunk.retryTimes >= maxRetries {
|
downloadChunk := func(chunk *chunk) (err error) {
|
||||||
return
|
// retry until all remain chunks failed
|
||||||
}
|
for {
|
||||||
|
// if chunk is completed, return
|
||||||
var (
|
if f.meta.Res.Range && chunk.remain() <= 0 {
|
||||||
httpReq *http.Request
|
return nil
|
||||||
resp *http.Response
|
|
||||||
)
|
|
||||||
httpReq, err = f.buildRequest(ctx, f.meta.Req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if f.meta.Res.Range {
|
|
||||||
httpReq.Header.Set(base.HttpHeaderRange,
|
|
||||||
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
|
||||||
} else {
|
|
||||||
chunk.Downloaded = 0
|
|
||||||
}
|
|
||||||
err = func() error {
|
|
||||||
resp, err = client.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
// if 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()
|
||||||
|
redirectURL := f.redirectURL
|
||||||
|
if redirectURL != "" {
|
||||||
|
f.redirectLock.Unlock()
|
||||||
|
} else {
|
||||||
|
// Only hold the lock when we need to potentially set redirectURL
|
||||||
|
defer func() {
|
||||||
|
if redirectURL == "" && err == nil {
|
||||||
|
f.redirectURL = resp.Request.URL.String()
|
||||||
|
}
|
||||||
|
f.redirectLock.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = func() (err error) {
|
||||||
|
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.retryTimes = chunk.retryTimes + 1
|
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
|
||||||
}
|
}
|
||||||
// if chunk is completed, reset other not complete chunks retry times
|
|
||||||
if err == nil {
|
remain := r.Chunk.remain()
|
||||||
for _, c := range f.chunks {
|
if remain > maxRemain && remain > helpMinSize {
|
||||||
if c != chunk && c.retryTimes > 0 {
|
maxRemainConnection = r
|
||||||
c.retryTimes = 0
|
maxRemain = remain
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -407,7 +542,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
if len(extra.Header) > 0 {
|
if len(extra.Header) > 0 {
|
||||||
for k, v := range extra.Header {
|
for k, v := range extra.Header {
|
||||||
headers.Set(k, v)
|
headers.Set(k, strings.TrimSpace(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if extra.Body != "" {
|
if extra.Body != "" {
|
||||||
@@ -415,13 +550,13 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||||
headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
|
headers.Set(base.HttpHeaderUserAgent, strings.TrimSpace(f.config.UserAgent))
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -434,30 +569,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
|
||||||
}
|
}
|
||||||
@@ -480,8 +618,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 {
|
||||||
@@ -537,7 +683,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,8 +695,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,36 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, "encoded-word", &base.Resource{
|
||||||
|
Size: test.BuildSize,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "测试.zip",
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, "no-encode", &base.Resource{
|
||||||
|
Size: test.BuildSize,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "测试.zip",
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
|
||||||
|
Size: 0,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "测试.zip",
|
||||||
|
Size: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
|
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Range: false,
|
Range: false,
|
||||||
@@ -75,6 +105,21 @@ func TestFetcher_ResolveWithHostHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveWithInvalidHeader(t *testing.T) {
|
||||||
|
fetcher := buildFetcher()
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "https://bing.com",
|
||||||
|
Extra: &http.ReqExtra{
|
||||||
|
Header: map[string]string{
|
||||||
|
"Referer": "\rtest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Resolve() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
||||||
listener := startTestServer()
|
listener := startTestServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -156,6 +201,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()
|
||||||
@@ -218,6 +271,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
|
||||||
|
|||||||
+117
-57
@@ -83,6 +83,36 @@ func StartTestCustomServer() net.Listener {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
io.Copy(writer, file)
|
io.Copy(writer, file)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/encoded-word", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Disposition", "attachment; filename=\"=?UTF8?B?5rWL6K+VLnppcA==?=\"")
|
||||||
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/%E6%B5%8B%E8%AF%95.zip", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/no-encode", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Disposition", "attachment; filename=测试.zip")
|
||||||
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -161,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
|
||||||
|
|||||||
+40
-32
@@ -2,15 +2,6 @@ package download
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/logger"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/pkgerrors"
|
|
||||||
"math"
|
"math"
|
||||||
gohttp "net/http"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -22,6 +13,16 @@ 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/internal/logger"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -148,6 +149,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 +210,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 +219,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 +360,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
|
||||||
}
|
}
|
||||||
@@ -527,7 +533,7 @@ func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
|
|||||||
|
|
||||||
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||||
if filter == nil || filter.IsEmpty() {
|
if filter == nil || filter.IsEmpty() {
|
||||||
return d.deleteAll()
|
return d.deleteAll(force)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTasks := d.GetTasksByFilter(filter)
|
deleteTasks := d.GetTasksByFilter(filter)
|
||||||
@@ -572,17 +578,21 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) deleteAll() (err error) {
|
func (d *Downloader) deleteAll(force bool) (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, force); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -892,18 +902,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 {
|
||||||
@@ -943,9 +942,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if enable white download directory, check if the download directory is in the white list
|
// if enable white download directory, check if the download directory is in the white list
|
||||||
if len(d.cfg.DownloadDirWhiteList) > 0 {
|
if len(d.cfg.WhiteDownloadDirs) > 0 {
|
||||||
inWhiteList := false
|
inWhiteList := false
|
||||||
for _, dir := range d.cfg.DownloadDirWhiteList {
|
for _, dir := range d.cfg.WhiteDownloadDirs {
|
||||||
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
||||||
inWhiteList = true
|
inWhiteList = true
|
||||||
break
|
break
|
||||||
@@ -1145,6 +1144,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 {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
downloader := NewDownloader(&DownloaderConfig{
|
downloader := NewDownloader(&DownloaderConfig{
|
||||||
DownloadDirWhiteList: []string{"./downloads"},
|
WhiteDownloadDirs: []string{"./downloads"},
|
||||||
})
|
})
|
||||||
if err := downloader.Setup(); err != nil {
|
if err := downloader.Setup(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -205,6 +205,31 @@ function testTimeout(){
|
|||||||
xhr.send();
|
xhr.send();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testFingerprint(fingerprint,ua){
|
||||||
|
__gopeed_setFingerprint(fingerprint);
|
||||||
|
const resp = await fetch(host+'/ua');
|
||||||
|
const data = await resp.json();
|
||||||
|
if(!data.user_agent.includes(ua)){
|
||||||
|
throw new Error('fingerprint test failed, user agent: ' + data.user_agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintDefault(){
|
||||||
|
await testFingerprint('none', 'Go')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintChrome(){
|
||||||
|
await testFingerprint('chrome', 'Chrome')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintFirefox(){
|
||||||
|
await testFingerprint('firefox', 'Firefox')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintSafari(){
|
||||||
|
await testFingerprint('safari', 'Safari')
|
||||||
|
}
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -288,6 +313,19 @@ function testTimeout(){
|
|||||||
if err == nil || err.Error() != "timeout" {
|
if err == nil || err.Error() != "timeout" {
|
||||||
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
|
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = callTestFun(engine, "testFingerprintChrome")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintChrome test failed", err)
|
||||||
|
}
|
||||||
|
_, err = callTestFun(engine, "testFingerprintFirefox")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintFirefox test failed", err)
|
||||||
|
}
|
||||||
|
_, err = callTestFun(engine, "testFingerprintSafari")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintSafari test failed", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchWithProxy(t *testing.T) {
|
func TestFetchWithProxy(t *testing.T) {
|
||||||
@@ -470,6 +508,15 @@ func startServer() net.Listener {
|
|||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/ua", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
data := map[string]any{
|
||||||
|
"user_agent": r.UserAgent(),
|
||||||
|
}
|
||||||
|
buf, _ := json.Marshal(data)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(buf)
|
||||||
|
})
|
||||||
server.Handler = mux
|
server.Handler = mux
|
||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
return listener
|
return listener
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -129,7 +130,8 @@ type XMLHttpRequest struct {
|
|||||||
requestHeaders http.Header
|
requestHeaders http.Header
|
||||||
responseHeaders http.Header
|
responseHeaders http.Header
|
||||||
aborted bool
|
aborted bool
|
||||||
proxyHandler func(r *http.Request) (*url.URL, error)
|
client *req.Client
|
||||||
|
fingerprint string
|
||||||
|
|
||||||
WithCredentials bool `json:"withCredentials"`
|
WithCredentials bool `json:"withCredentials"`
|
||||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||||
@@ -163,25 +165,39 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||||
var req *http.Request
|
setFingerprint(xhr.client, xhr.fingerprint)
|
||||||
var err error
|
|
||||||
d := xhr.parseData(data)
|
d := xhr.parseData(data)
|
||||||
var (
|
var (
|
||||||
contentType string
|
contentType string
|
||||||
contentLength int64
|
contentLength int64
|
||||||
isStringBody bool
|
isStringBody bool
|
||||||
)
|
)
|
||||||
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
// Create request using req library
|
||||||
} else {
|
reqBuilder := xhr.client.R()
|
||||||
|
|
||||||
|
// Set headers first
|
||||||
|
if xhr.requestHeaders != nil {
|
||||||
|
for key, values := range xhr.requestHeaders {
|
||||||
|
if len(values) > 0 {
|
||||||
|
// Merge multiple values with comma separator (HTTP standard)
|
||||||
|
mergedValue := strings.Join(values, ", ")
|
||||||
|
reqBuilder.SetHeader(key, mergedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle request body
|
||||||
|
if d != nil && xhr.method != "GET" && xhr.method != "HEAD" {
|
||||||
switch v := d.(type) {
|
switch v := d.(type) {
|
||||||
case string:
|
case string:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
|
reqBuilder.SetBody(v)
|
||||||
contentType = "text/plain;charset=UTF-8"
|
contentType = "text/plain;charset=UTF-8"
|
||||||
contentLength = int64(len(v))
|
contentLength = int64(len(v))
|
||||||
isStringBody = true
|
isStringBody = true
|
||||||
case *file.File:
|
case *file.File:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
|
reqBuilder.SetBody(v.Reader)
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
contentLength = v.Size
|
contentLength = v.Size
|
||||||
case *formdata.FormData:
|
case *formdata.FormData:
|
||||||
@@ -203,43 +219,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
defer mw.Close()
|
defer mw.Close()
|
||||||
mw.Send()
|
mw.Send()
|
||||||
}()
|
}()
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, pr)
|
reqBuilder.SetBody(pr)
|
||||||
contentType = mw.FormDataContentType()
|
contentType = mw.FormDataContentType()
|
||||||
contentLength = mw.Size()
|
contentLength = mw.Size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
xhr.callOnerror()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Header = xhr.requestHeaders
|
|
||||||
// Only string body can specify Content-Type header by user
|
// Only string body can specify Content-Type header by user
|
||||||
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
|
if contentType != "" && (!isStringBody || xhr.requestHeaders.Get("Content-Type") == "") {
|
||||||
req.Header.Set("Content-Type", contentType)
|
reqBuilder.SetHeader("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
if contentLength > 0 {
|
|
||||||
req.ContentLength = contentLength
|
// Set timeout
|
||||||
|
if xhr.Timeout > 0 {
|
||||||
|
xhr.client.SetTimeout(time.Duration(xhr.Timeout) * time.Millisecond)
|
||||||
}
|
}
|
||||||
transport := &http.Transport{
|
|
||||||
Proxy: xhr.proxyHandler,
|
// Configure redirect behavior
|
||||||
}
|
xhr.client.SetRedirectPolicy(func(req *http.Request, via []*http.Request) error {
|
||||||
client := &http.Client{
|
if xhr.Redirect == redirectManual {
|
||||||
Transport: transport,
|
return http.ErrUseLastResponse
|
||||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
}
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
if xhr.Redirect == redirectError {
|
||||||
if xhr.Redirect == redirectManual {
|
return errors.New("redirect failed")
|
||||||
return http.ErrUseLastResponse
|
}
|
||||||
}
|
if len(via) > 20 {
|
||||||
if xhr.Redirect == redirectError {
|
return errors.New("too many redirects")
|
||||||
return errors.New("redirect failed")
|
}
|
||||||
}
|
return nil
|
||||||
if len(via) > 20 {
|
})
|
||||||
return errors.New("too many redirects")
|
|
||||||
}
|
// Execute request
|
||||||
return nil
|
resp, err := reqBuilder.Send(xhr.method, xhr.url)
|
||||||
},
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// handle timeout error
|
// handle timeout error
|
||||||
var ne net.Error
|
var ne net.Error
|
||||||
@@ -254,35 +265,37 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
xhr.callOnerror()
|
xhr.callOnerror()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
xhr.Upload.callOnprogress(contentLength, contentLength)
|
xhr.Upload.callOnprogress(contentLength, contentLength)
|
||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.Upload.callOnload()
|
xhr.Upload.callOnload()
|
||||||
}
|
}
|
||||||
|
|
||||||
responseUrl := resp.Request.URL
|
// Set response URL (final URL after redirects)
|
||||||
responseUrl.Fragment = ""
|
if resp.Response != nil && resp.Response.Request != nil && resp.Response.Request.URL != nil {
|
||||||
xhr.ResponseUrl = responseUrl.String()
|
responseUrl := resp.Response.Request.URL
|
||||||
|
responseUrl.Fragment = ""
|
||||||
|
xhr.ResponseUrl = responseUrl.String()
|
||||||
|
} else {
|
||||||
|
xhr.ResponseUrl = xhr.url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set response headers
|
||||||
xhr.responseHeaders = resp.Header
|
xhr.responseHeaders = resp.Header
|
||||||
xhr.Status = resp.StatusCode
|
xhr.Status = resp.StatusCode
|
||||||
xhr.StatusText = resp.Status
|
xhr.StatusText = resp.Status
|
||||||
xhr.doReadystatechange(2)
|
xhr.doReadystatechange(2)
|
||||||
buf, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
bodyBytes := resp.Bytes()
|
||||||
xhr.callOnerror()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xhr.doReadystatechange(3)
|
xhr.doReadystatechange(3)
|
||||||
xhr.Response = string(buf)
|
xhr.Response = string(bodyBytes)
|
||||||
xhr.ResponseText = xhr.Response
|
xhr.ResponseText = xhr.Response
|
||||||
xhr.doReadystatechange(4)
|
xhr.doReadystatechange(4)
|
||||||
respBodyLen := int64(len(buf))
|
respBodyLen := int64(len(bodyBytes))
|
||||||
xhr.callOnprogress(respBodyLen, respBodyLen)
|
xhr.callOnprogress(respBodyLen, respBodyLen)
|
||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.callOnload()
|
xhr.callOnload()
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) Abort() {
|
func (xhr *XMLHttpRequest) Abort() {
|
||||||
@@ -293,6 +306,9 @@ func (xhr *XMLHttpRequest) Abort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
||||||
|
if xhr.responseHeaders == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +375,15 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
|||||||
return instanceValue
|
return instanceValue
|
||||||
})
|
})
|
||||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
// Create req client with proxy support
|
||||||
|
client := req.C()
|
||||||
|
if proxyHandler != nil {
|
||||||
|
client.SetProxy(proxyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
instance := &XMLHttpRequest{
|
instance := &XMLHttpRequest{
|
||||||
proxyHandler: proxyHandler,
|
client: client,
|
||||||
|
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
|
||||||
Upload: &XMLHttpRequestUpload{
|
Upload: &XMLHttpRequestUpload{
|
||||||
EventProp: &EventProp{
|
EventProp: &EventProp{
|
||||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||||
@@ -374,6 +397,9 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
|||||||
instanceValue.SetPrototype(call.This.Prototype())
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
return instanceValue
|
return instanceValue
|
||||||
})
|
})
|
||||||
|
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
|
||||||
|
runtime.Set(FingerprintMagicKey, fingerprint)
|
||||||
|
})
|
||||||
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package xhr
|
||||||
|
|
||||||
|
import "github.com/imroc/req/v3"
|
||||||
|
|
||||||
|
type Fingerprint string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
|
||||||
|
|
||||||
|
fingerprintChrome = "chrome"
|
||||||
|
fingerprintFirefox = "firefox"
|
||||||
|
fingerprintSafari = "safari"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setFingerprint(client *req.Client, fingerprint string) {
|
||||||
|
switch fingerprint {
|
||||||
|
case fingerprintChrome:
|
||||||
|
client.ImpersonateChrome()
|
||||||
|
case fingerprintFirefox:
|
||||||
|
client.ImpersonateFirefox()
|
||||||
|
case fingerprintSafari:
|
||||||
|
client.ImpersonateSafari()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,3 +22,16 @@ func AssertError[T error](err error) (t T, r bool) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SafeGet[T any](vm *goja.Runtime, name string) T {
|
||||||
|
v := vm.Get(name)
|
||||||
|
if v == nil {
|
||||||
|
var init T
|
||||||
|
return init
|
||||||
|
}
|
||||||
|
if e, ok := v.Export().(T); ok {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
var init T
|
||||||
|
return init
|
||||||
|
}
|
||||||
+21
-10
@@ -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 {
|
||||||
@@ -126,10 +137,10 @@ type DownloaderConfig struct {
|
|||||||
Controller *controller.Controller
|
Controller *controller.Controller
|
||||||
FetchManagers []fetcher.FetcherManager
|
FetchManagers []fetcher.FetcherManager
|
||||||
|
|
||||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
|
||||||
Storage Storage
|
Storage Storage
|
||||||
StorageDir string
|
StorageDir string
|
||||||
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
WhiteDownloadDirs []string
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
+4
-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
|
||||||
@@ -309,6 +309,8 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
r.RequestURI = ""
|
r.RequestURI = ""
|
||||||
r.URL = targetUrl
|
r.URL = targetUrl
|
||||||
r.Host = targetUrl.Host
|
r.Host = targetUrl.Host
|
||||||
|
r.Header.Del("Authorization")
|
||||||
|
r.Header.Del("X-Target-Uri")
|
||||||
resp, err := http.DefaultClient.Do(r)
|
resp, err := http.DefaultClient.Do(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, err.Error())
|
writeError(w, err.Error())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
@@ -25,9 +24,9 @@ type StartConfig struct {
|
|||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
WebEnable bool
|
WebEnable bool
|
||||||
WebFS fs.FS
|
WebFS fs.FS
|
||||||
WebBasicAuth *WebBasicAuth
|
WebAuth *WebAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *StartConfig) Init() *StartConfig {
|
func (cfg *StartConfig) Init() *StartConfig {
|
||||||
@@ -49,13 +48,7 @@ func (cfg *StartConfig) Init() *StartConfig {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebBasicAuth struct {
|
type WebAuth struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization returns the value of the Authorization header to be used in HTTP requests.
|
|
||||||
func (cfg *WebBasicAuth) Authorization() string {
|
|
||||||
userId := cfg.Username + ":" + cfg.Password
|
|
||||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
+206
-13
@@ -2,6 +2,10 @@ package rest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
@@ -10,17 +14,22 @@ 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"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
srv *http.Server
|
srv *http.Server
|
||||||
runningPort int
|
runningPort int
|
||||||
|
aesKey []byte
|
||||||
|
|
||||||
Downloader *download.Downloader
|
Downloader *download.Downloader
|
||||||
)
|
)
|
||||||
@@ -56,7 +65,9 @@ func Stop() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if srv != nil {
|
if srv != nil {
|
||||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,8 +85,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
startCfg.Init()
|
startCfg.Init()
|
||||||
|
|
||||||
downloadCfg := &download.DownloaderConfig{
|
downloadCfg := &download.DownloaderConfig{
|
||||||
ProductionMode: startCfg.ProductionMode,
|
ProductionMode: startCfg.ProductionMode,
|
||||||
RefreshInterval: startCfg.RefreshInterval,
|
RefreshInterval: startCfg.RefreshInterval,
|
||||||
|
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
|
||||||
}
|
}
|
||||||
if startCfg.Storage == model.StorageBolt {
|
if startCfg.Storage == model.StorageBolt {
|
||||||
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
||||||
@@ -93,6 +105,13 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
util.SafeRemove(startCfg.Address)
|
util.SafeRemove(startCfg.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if startCfg.WebEnable {
|
||||||
|
aesKey = make([]byte, 32)
|
||||||
|
if _, err := rand.Read(aesKey); err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "generate aes key failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -123,27 +142,94 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
||||||
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
||||||
|
|
||||||
|
enableApiToken := startCfg.ApiToken != ""
|
||||||
|
enableBasicAuth := startCfg.WebEnable && startCfg.WebAuth != nil
|
||||||
if startCfg.WebEnable {
|
if startCfg.WebEnable {
|
||||||
|
if enableBasicAuth {
|
||||||
|
r.Methods(http.MethodPost).Path("/api/web/login").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var loginReq model.WebAuth
|
||||||
|
if ReadJson(r, w, &loginReq) {
|
||||||
|
if loginReq.Username == startCfg.WebAuth.Username && loginReq.Password == startCfg.WebAuth.Password {
|
||||||
|
// Generate a login token, Username:Password:Timestamp
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
tokenData := fmt.Sprintf("%s:%s:%d", loginReq.Username, loginReq.Password, timestamp)
|
||||||
|
token, err := aesEncrypt(aesKey, []byte(tokenData))
|
||||||
|
if err != nil {
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJson(w, model.NewOkResult(token))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
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 enableApiToken || enableBasicAuth {
|
||||||
|
writeUnauthorized := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||||
|
}
|
||||||
|
|
||||||
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
|
||||||
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
if enableBasicAuth {
|
||||||
|
if !strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api/web/login" {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
|
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token = strings.TrimPrefix(token, "Bearer ")
|
||||||
|
tokenData, err := aesDecrypt(aesKey, token)
|
||||||
|
if err != nil {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(string(tokenData), ":", 3)
|
||||||
|
username := parts[0]
|
||||||
|
password := parts[1]
|
||||||
|
timestamp, _ := strconv.Atoi(parts[2])
|
||||||
|
|
||||||
|
if username != startCfg.WebAuth.Username || password != startCfg.WebAuth.Password {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the token is expired (7 days)
|
||||||
|
if time.Now().Unix()-int64(timestamp) > 7*24*3600 {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
writeUnauthorized(w, r)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -163,9 +249,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
})
|
})
|
||||||
|
|
||||||
srv = &http.Server{Handler: handlers.CORS(
|
srv = &http.Server{Handler: handlers.CORS(
|
||||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "X-Api-Token", "X-Target-Uri"}),
|
||||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||||
handlers.AllowedOrigins([]string{"*"}),
|
handlers.AllowedOrigins([]string{"*"}),
|
||||||
|
handlers.AllowCredentials(),
|
||||||
)(r)}
|
)(r)}
|
||||||
return srv, listener, nil
|
return srv, listener, nil
|
||||||
}
|
}
|
||||||
@@ -222,6 +309,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()))
|
||||||
@@ -239,3 +388,47 @@ func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
|
|||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
json.NewEncoder(w).Encode(v)
|
json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func aesEncrypt(key, data []byte) (string, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherText := gcm.Seal(nonce, nonce, data, nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(cipherText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func aesDecrypt(key []byte, encryptedData string) ([]byte, error) {
|
||||||
|
cipherText, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cipherText) < gcm.NonceSize() {
|
||||||
|
return nil, errors.New("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, cipherText := cipherText[:gcm.NonceSize()], cipherText[gcm.NonceSize():]
|
||||||
|
return gcm.Open(nil, nonce, cipherText, nil)
|
||||||
|
}
|
||||||
+192
-7
@@ -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{
|
||||||
@@ -534,7 +637,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
cfg.Init()
|
cfg.Init()
|
||||||
cfg.ApiToken = "123456"
|
cfg.ApiToken = "123456"
|
||||||
cfg.WebEnable = true
|
cfg.WebEnable = true
|
||||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
cfg.WebAuth = &model.WebAuth{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Password: "123456",
|
Password: "123456",
|
||||||
}
|
}
|
||||||
@@ -546,14 +649,61 @@ func TestAuthorization(t *testing.T) {
|
|||||||
Stop()
|
Stop()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
status, _ := doHttpRequest0(http.MethodPost, "/api/web/login", nil, &model.WebAuth{
|
||||||
|
Username: "xxx",
|
||||||
|
Password: "xxx",
|
||||||
|
})
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := httpRequestCheckOk[string](http.MethodPost, "/api/web/login", cfg.WebAuth)
|
||||||
|
authToken := fmt.Sprintf("Bearer %s", token)
|
||||||
|
authHeaders := map[string]string{
|
||||||
|
"Authorization": authToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||||
if status != http.StatusUnauthorized {
|
if status != http.StatusUnauthorized {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
"Authorization": "xxx",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": "xxx",
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildToken := func(username, password string, ts int64) string {
|
||||||
|
token, _ := aesEncrypt(aesKey, []byte(fmt.Sprintf("%s:%s:%d", username, password, ts)))
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeToken := buildToken("fake", "fake", time.Now().Unix())
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", fakeToken),
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
expireToken := buildToken(cfg.WebAuth.Username, cfg.WebAuth.Password, time.Now().Add(-time.Hour*8*24).Unix())
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", expireToken),
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", authHeaders, nil)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -564,15 +714,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": authToken,
|
||||||
|
"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": authToken,
|
||||||
|
"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 +755,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 +777,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 +802,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]) {
|
||||||
|
|||||||
@@ -56,3 +56,13 @@ func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error)
|
|||||||
}
|
}
|
||||||
return http.ProxyURL(proxyUrl)
|
return http.ProxyURL(proxyUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TryUrlQueryUnescape tries to unescape a URL-encoded string.
|
||||||
|
//
|
||||||
|
// If unescaping fails, it returns the original string.
|
||||||
|
func TryUrlQueryUnescape(s string) string {
|
||||||
|
if decoded, err := url.QueryUnescape(s); err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -128,3 +128,24 @@ func TestParseDataUri(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTryURLDecode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"normal.txt", "normal.txt"},
|
||||||
|
{"%E7%8F%80%E5%B0%94%E8%AF%BA.zip", "珀尔诺.zip"},
|
||||||
|
{"hello%20world.txt", "hello world.txt"},
|
||||||
|
{"bad%2-text", "bad%2-text"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
got := TryUrlQueryUnescape(tt.input)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("TryUrlQueryUnescape(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,5 +69,7 @@ windows/flutter/generated_plugins.cmake
|
|||||||
database.hive
|
database.hive
|
||||||
database.lock
|
database.lock
|
||||||
|
|
||||||
assets/host/host
|
assets/exec/host
|
||||||
assets/host/host.exe
|
assets/exec/host.exe
|
||||||
|
assets/exec/updater
|
||||||
|
assets/exec/updater.exe
|
||||||
@@ -48,7 +48,7 @@ android {
|
|||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -91,7 +93,8 @@
|
|||||||
<!-- Add android:stopWithTask option only when necessary. -->
|
<!-- Add android:stopWithTask option only when necessary. -->
|
||||||
<service
|
<service
|
||||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||||
android:stopWithTask="true" />
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
|||||||
File renamed without changes.
+68
-32
@@ -3,19 +3,24 @@ 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:get/get.dart' as getx;
|
||||||
import 'package:gopeed/api/model/install_extension.dart';
|
|
||||||
import 'package:gopeed/api/model/switch_extension.dart';
|
|
||||||
|
|
||||||
|
import '../app/routes/app_pages.dart';
|
||||||
|
import '../database/database.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/login.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 +34,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;
|
||||||
@@ -42,12 +47,27 @@ class _Client {
|
|||||||
dio.options.sendTimeout = const Duration(seconds: 5);
|
dio.options.sendTimeout = const Duration(seconds: 5);
|
||||||
dio.options.connectTimeout = const Duration(seconds: 5);
|
dio.options.connectTimeout = const Duration(seconds: 5);
|
||||||
dio.options.receiveTimeout = const Duration(seconds: 60);
|
dio.options.receiveTimeout = const Duration(seconds: 60);
|
||||||
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
|
dio.interceptors.add(InterceptorsWrapper(
|
||||||
if (apiToken.isNotEmpty) {
|
onRequest: (options, handler) {
|
||||||
options.headers['X-Api-Token'] = apiToken;
|
if (apiToken.isNotEmpty) {
|
||||||
}
|
options.headers['X-Api-Token'] = apiToken;
|
||||||
handler.next(options);
|
}
|
||||||
}));
|
if (Util.isWeb()) {
|
||||||
|
final token = Database.instance.getWebToken();
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.next(options);
|
||||||
|
},
|
||||||
|
onError: (error, handler) {
|
||||||
|
// Only web version has a login page
|
||||||
|
if (Util.isWeb() && error.response?.statusCode == 401) {
|
||||||
|
getx.Get.rootDelegate.offAndToNamed(Routes.LOGIN);
|
||||||
|
}
|
||||||
|
handler.next(error);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
_instance!.dio = dio;
|
_instance!.dio = dio;
|
||||||
if (isUnixSocket) {
|
if (isUnixSocket) {
|
||||||
@@ -95,7 +115,8 @@ Future<T> _parse<T>(
|
|||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.type == DioExceptionType.sendTimeout ||
|
if (e.type == DioExceptionType.sendTimeout ||
|
||||||
e.type == DioExceptionType.receiveTimeout ||
|
e.type == DioExceptionType.receiveTimeout ||
|
||||||
e.type == DioExceptionType.connectionTimeout) {
|
e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
throw TimeoutException("request timeout");
|
throw TimeoutException("request timeout");
|
||||||
}
|
}
|
||||||
throw Exception(Result(code: 1000, msg: e.message));
|
throw Exception(Result(code: 1000, msg: e.message));
|
||||||
@@ -104,16 +125,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 +149,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 +166,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 +174,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 +187,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 +218,27 @@ 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<String> login(LoginReq loginReq) async {
|
||||||
|
return _parse(() => _client.dio.post("api/web/login", data: loginReq),
|
||||||
|
(data) => data as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response<String>> proxyRequest<T>(String uri,
|
Future<Response<String>> proxyRequest<T>(String uri,
|
||||||
@@ -223,5 +255,9 @@ Future<Response<String>> proxyRequest<T>(String uri,
|
|||||||
}
|
}
|
||||||
|
|
||||||
String join(String path) {
|
String join(String path) {
|
||||||
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}";
|
final baseUrl = _client.dio.options.baseUrl;
|
||||||
|
final cleanBaseUrl = baseUrl.endsWith('/')
|
||||||
|
? baseUrl.substring(0, baseUrl.length - 1)
|
||||||
|
: baseUrl;
|
||||||
|
return "$cleanBaseUrl/${Util.cleanPath(path)}";
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ class ExtraConfig {
|
|||||||
bool lastDeleteTaskKeep;
|
bool lastDeleteTaskKeep;
|
||||||
bool defaultDirectDownload;
|
bool defaultDirectDownload;
|
||||||
bool defaultBtClient;
|
bool defaultBtClient;
|
||||||
|
bool notifyWhenNewVersion;
|
||||||
|
|
||||||
ExtraConfigBt bt = ExtraConfigBt();
|
ExtraConfigBt bt = ExtraConfigBt();
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ class ExtraConfig {
|
|||||||
this.lastDeleteTaskKeep = false,
|
this.lastDeleteTaskKeep = false,
|
||||||
this.defaultDirectDownload = false,
|
this.defaultDirectDownload = false,
|
||||||
this.defaultBtClient = true,
|
this.defaultBtClient = true,
|
||||||
|
this.notifyWhenNewVersion = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
|||||||
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
||||||
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
||||||
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
|
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
|
||||||
|
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
|
||||||
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||||
|
|
||||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||||
@@ -83,6 +84,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
|||||||
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||||
'defaultDirectDownload': instance.defaultDirectDownload,
|
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||||
'defaultBtClient': instance.defaultBtClient,
|
'defaultBtClient': instance.defaultBtClient,
|
||||||
|
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
|
||||||
'bt': instance.bt.toJson(),
|
'bt': instance.bt.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'login.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class LoginReq {
|
||||||
|
String username;
|
||||||
|
String password;
|
||||||
|
|
||||||
|
LoginReq({
|
||||||
|
required this.username,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginReq.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LoginReqFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$LoginReqToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'login.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
LoginReq _$LoginReqFromJson(Map<String, dynamic> json) => LoginReq(
|
||||||
|
username: json['username'] as String,
|
||||||
|
password: json['password'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LoginReqToJson(LoginReq instance) => <String, dynamic>{
|
||||||
|
'username': instance.username,
|
||||||
|
'password': instance.password,
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
|
import '../../../../api/model/create_task.dart';
|
||||||
import '../../../../api/model/downloader_config.dart';
|
import '../../../../api/model/downloader_config.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
import '../../../../core/common/start_config.dart';
|
import '../../../../core/common/start_config.dart';
|
||||||
@@ -24,58 +25,35 @@ import '../../../../database/database.dart';
|
|||||||
import '../../../../database/entity.dart';
|
import '../../../../database/entity.dart';
|
||||||
import '../../../../i18n/message.dart';
|
import '../../../../i18n/message.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
|
import '../../../../util/github_mirror.dart';
|
||||||
import '../../../../util/locale_manager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
import '../../../../util/log_util.dart';
|
import '../../../../util/log_util.dart';
|
||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
|
import '../../../../util/updater.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
import '../../create/dto/create_router_params.dart';
|
import '../../../rpc/rpc.dart';
|
||||||
import '../../redirect/views/redirect_view.dart';
|
import '../../redirect/views/redirect_view.dart';
|
||||||
|
|
||||||
const unixSocketPath = 'gopeed.sock';
|
const unixSocketPath = 'gopeed.sock';
|
||||||
|
|
||||||
const allTrackerSubscribeUrls = [
|
const allTrackerSubscribeUrls = [
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_http.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_http.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_https.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_https.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ip.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_udp.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_udp.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ws.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ws.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best_ip.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt',
|
||||||
'https://github.com/XIU2/TrackersListCollection/raw/master/all.txt',
|
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt',
|
||||||
'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt',
|
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt',
|
||||||
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
|
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/http.txt',
|
||||||
];
|
];
|
||||||
const allTrackerCdns = [
|
final allTrackerSubscribeUrlCdns = {
|
||||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
for (var v in allTrackerSubscribeUrls)
|
||||||
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
|
v: githubMirrorUrls(v, MirrorType.githubSource)
|
||||||
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
};
|
||||||
[
|
|
||||||
"https://ghproxy.com/https://raw.githubusercontent.com",
|
|
||||||
r".*github.com(/.*)/raw(/.*)"
|
|
||||||
],
|
|
||||||
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
|
||||||
[
|
|
||||||
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
|
|
||||||
r".*github.com(/.*)/raw(/.*)"
|
|
||||||
],
|
|
||||||
];
|
|
||||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
|
||||||
key: (v) => v as String,
|
|
||||||
value: (v) {
|
|
||||||
final ret = [v as String];
|
|
||||||
for (final cdn in allTrackerCdns) {
|
|
||||||
final reg = RegExp(cdn[1]);
|
|
||||||
final match = reg.firstMatch(v.toString());
|
|
||||||
var matchStr = "";
|
|
||||||
for (var i = 1; i <= match!.groupCount; i++) {
|
|
||||||
matchStr += match.group(i)!;
|
|
||||||
}
|
|
||||||
ret.add("${cdn[0]}$matchStr");
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
|
|
||||||
class AppController extends GetxController with WindowListener, TrayListener {
|
class AppController extends GetxController with WindowListener, TrayListener {
|
||||||
static StartConfig? _defaultStartConfig;
|
static StartConfig? _defaultStartConfig;
|
||||||
@@ -101,6 +79,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
_initTray().onError(
|
_initTray().onError(
|
||||||
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
||||||
|
|
||||||
|
_initRpcServer().onError((error, stackTrace) =>
|
||||||
|
logger.w("initRpcServer error", error, stackTrace));
|
||||||
|
|
||||||
_initForegroundTask().onError((error, stackTrace) =>
|
_initForegroundTask().onError((error, stackTrace) =>
|
||||||
logger.w("initForegroundTask error", error, stackTrace));
|
logger.w("initForegroundTask error", error, stackTrace));
|
||||||
|
|
||||||
@@ -109,6 +90,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
_initLaunchAtStartup().onError((error, stackTrace) =>
|
_initLaunchAtStartup().onError((error, stackTrace) =>
|
||||||
logger.w("initLaunchAtStartup error", error, stackTrace));
|
logger.w("initLaunchAtStartup error", error, stackTrace));
|
||||||
|
|
||||||
|
_initCheckUpdate().onError((error, stackTrace) =>
|
||||||
|
logger.w("initCheckUpdate error", error, stackTrace));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -139,7 +123,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onTrayIconRightMouseDown() {
|
void onTrayIconRightMouseDown() {
|
||||||
trayManager.popUpContextMenu();
|
trayManager.popUpContextMenu(bringAppToFront: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -291,6 +275,37 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
trayManager.addListener(this);
|
trayManager.addListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initRpcServer() async {
|
||||||
|
if (!Util.isDesktop()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await startRpcServer({
|
||||||
|
"/create": (ctx) async {
|
||||||
|
final meta =
|
||||||
|
ctx.request.headers["X-Gopeed-Host-Meta"]?.firstOrNull ?? "{}";
|
||||||
|
final jsonMeta = jsonDecode(meta);
|
||||||
|
final silent = jsonMeta['silent'] as bool? ?? false;
|
||||||
|
final params = await ctx.readText();
|
||||||
|
final createTaskParams = _decodeToCreatTaskParams(params);
|
||||||
|
if (!silent) {
|
||||||
|
await windowManager.show();
|
||||||
|
_handleToCreate0(createTaskParams);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await createTask(createTaskParams);
|
||||||
|
} catch (e) {
|
||||||
|
logger.w(
|
||||||
|
"create task from extension fail", e, StackTrace.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.w("start rpc server fail", e, StackTrace.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initForegroundTask() async {
|
Future<void> _initForegroundTask() async {
|
||||||
if (!Util.isMobile()) {
|
if (!Util.isMobile()) {
|
||||||
return;
|
return;
|
||||||
@@ -337,12 +352,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
if (uri.path == "/create") {
|
if (uri.path == "/create") {
|
||||||
final params = uri.queryParameters["params"];
|
final params = uri.queryParameters["params"];
|
||||||
if (params?.isNotEmpty == true) {
|
if (params?.isNotEmpty == true) {
|
||||||
final paramsJson =
|
_handleToCreate(params!);
|
||||||
String.fromCharCodes(base64Decode(base64.normalize(params!)));
|
|
||||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
|
||||||
arguments: RedirectArgs(Routes.CREATE,
|
|
||||||
arguments:
|
|
||||||
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Get.rootDelegate.offAndToNamed(Routes.CREATE);
|
Get.rootDelegate.offAndToNamed(Routes.CREATE);
|
||||||
@@ -358,13 +368,14 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
uri.scheme == "https") {
|
uri.scheme == "https") {
|
||||||
path = uri.toString();
|
path = uri.toString();
|
||||||
} else if (uri.scheme == "file") {
|
} else if (uri.scheme == "file") {
|
||||||
path = Util.isWindows() ? uri.path.substring(1) : uri.path;
|
path =
|
||||||
|
Util.isWindows() ? Uri.decodeFull(uri.path.substring(1)) : uri.path;
|
||||||
} else {
|
} else {
|
||||||
path = (await toFile(uri.toString())).path;
|
path = (await toFile(uri.toString())).path;
|
||||||
}
|
}
|
||||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||||
arguments: RedirectArgs(Routes.CREATE,
|
arguments: RedirectArgs(Routes.CREATE,
|
||||||
arguments: CreateRouterParams(req: Request(url: path))));
|
arguments: CreateTask(req: Request(url: path))));
|
||||||
}
|
}
|
||||||
|
|
||||||
String runningAddress() {
|
String runningAddress() {
|
||||||
@@ -523,6 +534,18 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
autoStartup.value = await launchAtStartup.isEnabled();
|
autoStartup.value = await launchAtStartup.isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initCheckUpdate() async {
|
||||||
|
// Check if auto check update is enabled
|
||||||
|
if (!downloaderConfig.value.extra.notifyWhenNewVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final versionInfo = await checkUpdate();
|
||||||
|
if (versionInfo != null) {
|
||||||
|
await showUpdateDialog(Get.context!, versionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveConfig() async {
|
Future<void> saveConfig() async {
|
||||||
Database.instance.saveStartConfig(StartConfigEntity(
|
Database.instance.saveStartConfig(StartConfigEntity(
|
||||||
network: startConfig.value.network,
|
network: startConfig.value.network,
|
||||||
@@ -530,4 +553,21 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
apiToken: startConfig.value.apiToken));
|
apiToken: startConfig.value.apiToken));
|
||||||
await putConfig(downloaderConfig.value);
|
await putConfig(downloaderConfig.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateTask _decodeToCreatTaskParams(String params) {
|
||||||
|
final safeParams = params.replaceAll('"', "").replaceAll(" ", "+");
|
||||||
|
final paramsJson =
|
||||||
|
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
|
||||||
|
return CreateTask.fromJson(jsonDecode(paramsJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleToCreate(String params) {
|
||||||
|
final createTaskParams = _decodeToCreatTaskParams(params);
|
||||||
|
_handleToCreate0(createTaskParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleToCreate0(CreateTask createTaskParams) {
|
||||||
|
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||||
|
arguments: RedirectArgs(Routes.CREATE, arguments: createTaskParams));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart'; // Import the required packages
|
||||||
|
|
||||||
import '../../../../i18n/message.dart';
|
import '../../../../i18n/message.dart';
|
||||||
import '../../../../theme/theme.dart';
|
import '../../../../theme/theme.dart';
|
||||||
import '../../../../util/locale_manager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
|
import '../../../../util/util.dart'; // Import the required packages
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
import '../controllers/app_controller.dart';
|
import '../controllers/app_controller.dart';
|
||||||
|
|
||||||
@@ -32,6 +34,19 @@ class AppView extends GetView<AppController> {
|
|||||||
],
|
],
|
||||||
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
||||||
getPages: AppPages.routes,
|
getPages: AppPages.routes,
|
||||||
|
|
||||||
|
// Add listening to theme changes, set the title bar color according to the current system theme.
|
||||||
|
builder: (context, child) {
|
||||||
|
// if platform is desktop
|
||||||
|
if (Util.isDesktop()){
|
||||||
|
// actual brightness of the UI
|
||||||
|
Brightness brightness = Theme.of(context).brightness;
|
||||||
|
// Set the title bar to use the actual brightness of the UI
|
||||||
|
windowManager.setBrightness(brightness);
|
||||||
|
}
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
import '../../../../api/model/options.dart';
|
|
||||||
import '../../../../api/model/request.dart';
|
|
||||||
|
|
||||||
part 'create_router_params.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
|
||||||
class CreateRouterParams {
|
|
||||||
Request? req;
|
|
||||||
Options? opt;
|
|
||||||
|
|
||||||
CreateRouterParams({
|
|
||||||
this.req,
|
|
||||||
this.opt,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory CreateRouterParams.fromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) =>
|
|
||||||
_$CreateRouterParamsFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$CreateRouterParamsToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'create_router_params.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
CreateRouterParams _$CreateRouterParamsFromJson(Map<String, dynamic> json) =>
|
|
||||||
CreateRouterParams(
|
|
||||||
req: json['req'] == null
|
|
||||||
? null
|
|
||||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
|
||||||
opt: json['opt'] == null
|
|
||||||
? null
|
|
||||||
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CreateRouterParamsToJson(CreateRouterParams instance) {
|
|
||||||
final val = <String, dynamic>{};
|
|
||||||
|
|
||||||
void writeNotNull(String key, dynamic value) {
|
|
||||||
if (value != null) {
|
|
||||||
val[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeNotNull('req', instance.req?.toJson());
|
|
||||||
writeNotNull('opt', instance.opt?.toJson());
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
@@ -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';
|
||||||
@@ -26,7 +27,6 @@ import '../../../views/file_tree_view.dart';
|
|||||||
import '../../app/controllers/app_controller.dart';
|
import '../../app/controllers/app_controller.dart';
|
||||||
import '../../history/views/history_view.dart';
|
import '../../history/views/history_view.dart';
|
||||||
import '../controllers/create_controller.dart';
|
import '../controllers/create_controller.dart';
|
||||||
import '../dto/create_router_params.dart';
|
|
||||||
|
|
||||||
class CreateView extends GetView<CreateController> {
|
class CreateView extends GetView<CreateController> {
|
||||||
final _confirmFormKey = GlobalKey<FormState>();
|
final _confirmFormKey = GlobalKey<FormState>();
|
||||||
@@ -75,23 +75,14 @@ class CreateView extends GetView<CreateController> {
|
|||||||
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
|
final CreateTask? routerParams = Get.rootDelegate.arguments();
|
||||||
if (routerParams?.req?.url.isNotEmpty ?? false) {
|
if (routerParams?.req?.url.isNotEmpty ?? false) {
|
||||||
// get url from route arguments
|
// get url from route arguments
|
||||||
final url = routerParams!.req!.url;
|
final url = routerParams!.req!.url;
|
||||||
_urlController.text = url;
|
_urlController.text = url;
|
||||||
_urlController.selection = TextSelection.fromPosition(
|
_urlController.selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: _urlController.text.length));
|
TextPosition(offset: _urlController.text.length));
|
||||||
final uppercaseUrl = url.toUpperCase();
|
final protocol = parseProtocol(url);
|
||||||
Protocol? protocol;
|
|
||||||
if (uppercaseUrl.startsWith("HTTP:") ||
|
|
||||||
uppercaseUrl.startsWith("HTTPS:")) {
|
|
||||||
protocol = Protocol.http;
|
|
||||||
}
|
|
||||||
if (uppercaseUrl.startsWith("MAGNET:") ||
|
|
||||||
uppercaseUrl.endsWith(".TORRENT")) {
|
|
||||||
protocol = Protocol.bt;
|
|
||||||
}
|
|
||||||
if (protocol != null) {
|
if (protocol != null) {
|
||||||
final extraHandlers = {
|
final extraHandlers = {
|
||||||
Protocol.http: () {
|
Protocol.http: () {
|
||||||
@@ -672,6 +663,20 @@ class CreateView extends GetView<CreateController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse protocol from url
|
||||||
|
parseProtocol(String url) {
|
||||||
|
final uppercaseUrl = url.toUpperCase();
|
||||||
|
Protocol? protocol;
|
||||||
|
if (uppercaseUrl.startsWith("HTTP:") || uppercaseUrl.startsWith("HTTPS:")) {
|
||||||
|
protocol = Protocol.http;
|
||||||
|
}
|
||||||
|
if (uppercaseUrl.startsWith("MAGNET:") ||
|
||||||
|
uppercaseUrl.endsWith(".TORRENT")) {
|
||||||
|
protocol = Protocol.bt;
|
||||||
|
}
|
||||||
|
return protocol;
|
||||||
|
}
|
||||||
|
|
||||||
// recognize magnet uri, if length == 40, auto add magnet prefix
|
// recognize magnet uri, if length == 40, auto add magnet prefix
|
||||||
recognizeMagnetUri(String text) {
|
recognizeMagnetUri(String text) {
|
||||||
if (text.length != 40) {
|
if (text.length != 40) {
|
||||||
@@ -764,24 +769,23 @@ class CreateView extends GetView<CreateController> {
|
|||||||
|
|
||||||
Object? parseReqExtra(String url) {
|
Object? parseReqExtra(String url) {
|
||||||
Object? reqExtra;
|
Object? reqExtra;
|
||||||
if (controller.showAdvanced.value) {
|
final protocol = parseProtocol(url);
|
||||||
switch (controller.advancedTabController.index) {
|
switch (protocol) {
|
||||||
case 0:
|
case Protocol.http:
|
||||||
final header = Map<String, String>.fromEntries(_httpHeaderControllers
|
final header = Map<String, String>.fromEntries(_httpHeaderControllers
|
||||||
.map((e) => MapEntry(e.name.text, e.value.text)));
|
.map((e) => MapEntry(e.name.text, e.value.text)));
|
||||||
header.removeWhere(
|
header.removeWhere(
|
||||||
(key, value) => key.trim().isEmpty || value.trim().isEmpty);
|
(key, value) => key.trim().isEmpty || value.trim().isEmpty);
|
||||||
if (header.isNotEmpty) {
|
if (header.isNotEmpty) {
|
||||||
reqExtra = ReqExtraHttp()..header = header;
|
reqExtra = ReqExtraHttp()..header = header;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 1:
|
case Protocol.bt:
|
||||||
if (_btTrackerController.text.trim().isNotEmpty) {
|
if (_btTrackerController.text.trim().isNotEmpty) {
|
||||||
reqExtra = ReqExtraBt()
|
reqExtra = ReqExtraBt()
|
||||||
..trackers = Util.textToLines(_btTrackerController.text);
|
..trackers = Util.textToLines(_btTrackerController.text);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return reqExtra;
|
return reqExtra;
|
||||||
}
|
}
|
||||||
@@ -865,18 +869,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,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '../../../../api/model/extension.dart';
|
|||||||
import '../../../../api/model/install_extension.dart';
|
import '../../../../api/model/install_extension.dart';
|
||||||
import '../../../../api/model/switch_extension.dart';
|
import '../../../../api/model/switch_extension.dart';
|
||||||
import '../../../../api/model/update_extension_settings.dart';
|
import '../../../../api/model/update_extension_settings.dart';
|
||||||
|
import '../../../../database/database.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../views/icon_button_loading.dart';
|
import '../../../views/icon_button_loading.dart';
|
||||||
@@ -135,6 +136,10 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
|
headers: {
|
||||||
|
'Authorization':
|
||||||
|
'Bearer ${Database.instance.getWebToken()}'
|
||||||
|
},
|
||||||
)
|
)
|
||||||
: Image.file(
|
: Image.file(
|
||||||
extension.devMode
|
extension.devMode
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../controllers/login_controller.dart';
|
||||||
|
|
||||||
|
class LoginBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
Get.lazyPut<LoginController>(
|
||||||
|
() => LoginController(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../../../api/api.dart' as api;
|
||||||
|
import '../../../../api/api.dart';
|
||||||
|
import '../../../../api/model/login.dart';
|
||||||
|
import '../../../../database/database.dart';
|
||||||
|
import '../../../../util/message.dart';
|
||||||
|
import '../../../routes/app_pages.dart';
|
||||||
|
import '../../app/controllers/app_controller.dart';
|
||||||
|
|
||||||
|
class LoginController extends GetxController {
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
final usernameController = TextEditingController();
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
|
||||||
|
final isLoading = false.obs;
|
||||||
|
final passwordVisible = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
usernameController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void togglePasswordVisibility() {
|
||||||
|
passwordVisible.value = !passwordVisible.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> login() async {
|
||||||
|
if (!formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
final loginReq = LoginReq(
|
||||||
|
username: usernameController.text.trim(),
|
||||||
|
password: passwordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
final token = await api.login(loginReq);
|
||||||
|
// Login successful, save the token
|
||||||
|
Database.instance.saveWebToken(token);
|
||||||
|
// Reload config
|
||||||
|
final controller = Get.put(AppController());
|
||||||
|
await controller.loadDownloaderConfig();
|
||||||
|
// Navigate to home page
|
||||||
|
Get.rootDelegate.offAndToNamed(Routes.HOME);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is TimeoutException) {
|
||||||
|
showMessage('error'.tr, 'login_failed_network'.tr);
|
||||||
|
} else {
|
||||||
|
showMessage('error'.tr, 'login_failed'.tr);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../../views/responsive_builder.dart';
|
||||||
|
import '../controllers/login_controller.dart';
|
||||||
|
|
||||||
|
class LoginView extends GetView<LoginController> {
|
||||||
|
const LoginView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isNarrow = ResponsiveBuilder.isNarrow(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Get.theme.colorScheme.surface,
|
||||||
|
Get.theme.colorScheme.surface.withOpacity(0.8),
|
||||||
|
Get.theme.colorScheme.surface.withOpacity(0.9),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: isNarrow ? 16.0 : 32.0,
|
||||||
|
vertical: isNarrow ? 24.0 : 32.0,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
elevation: 12,
|
||||||
|
shadowColor: Get.theme.colorScheme.shadow.withOpacity(0.3),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(isNarrow ? 20 : 24),
|
||||||
|
),
|
||||||
|
color: Get.theme.colorScheme.surface,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: isNarrow ? double.infinity : 420,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(isNarrow ? 24.0 : 40.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(isNarrow ? 20 : 24),
|
||||||
|
border: Border.all(
|
||||||
|
color: Get.theme.colorScheme.outline.withOpacity(0.1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: FocusTraversalGroup(
|
||||||
|
policy: OrderedTraversalPolicy(),
|
||||||
|
child: Form(
|
||||||
|
key: controller.formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Logo/Title Section
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
margin: EdgeInsets.only(bottom: isNarrow ? 32 : 48),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(isNarrow ? 8 : 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(isNarrow ? 20 : 24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.2),
|
||||||
|
blurRadius: isNarrow ? 16 : 24,
|
||||||
|
offset: Offset(0, isNarrow ? 8 : 12),
|
||||||
|
spreadRadius: isNarrow ? 1 : 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(isNarrow ? 16 : 20),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/icon/icon.svg',
|
||||||
|
width: isNarrow ? 56 : 72,
|
||||||
|
height: isNarrow ? 56 : 72,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Gopeed',
|
||||||
|
style: Get.textTheme.headlineLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Get.theme.colorScheme.onSurface,
|
||||||
|
letterSpacing: 2.0,
|
||||||
|
fontSize: isNarrow ? 28 : 36,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Username Field
|
||||||
|
FocusTraversalOrder(
|
||||||
|
order: const NumericFocusOrder(1.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller.usernameController,
|
||||||
|
autofillHints: const [
|
||||||
|
AutofillHints.username,
|
||||||
|
],
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onFieldSubmitted: (_) => controller.login(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'username'.tr,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Get.theme.colorScheme.onSurface
|
||||||
|
.withOpacity(0.7),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
prefixIcon: Container(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.person_outline_rounded,
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.3),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Get
|
||||||
|
.theme.colorScheme.surfaceContainerHighest
|
||||||
|
.withOpacity(0.8),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'username_required'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: isNarrow ? 16 : 24),
|
||||||
|
|
||||||
|
// Password Field
|
||||||
|
FocusTraversalOrder(
|
||||||
|
order: const NumericFocusOrder(2.0),
|
||||||
|
child: Obx(() => TextFormField(
|
||||||
|
controller: controller.passwordController,
|
||||||
|
autofillHints: const [
|
||||||
|
AutofillHints.password,
|
||||||
|
],
|
||||||
|
obscureText: !controller.passwordVisible.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'password'.tr,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Get.theme.colorScheme.onSurface
|
||||||
|
.withOpacity(0.7),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
prefixIcon: Container(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.lock_outline_rounded,
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
controller.passwordVisible.value
|
||||||
|
? Icons.visibility_off_rounded
|
||||||
|
: Icons.visibility_rounded,
|
||||||
|
color: Get.theme.colorScheme.onSurface
|
||||||
|
.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
controller.togglePasswordVisibility,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.3),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Get
|
||||||
|
.theme.colorScheme.surfaceContainerHighest
|
||||||
|
.withOpacity(0.8),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'password_required'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onFieldSubmitted: (_) => controller.login(),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: isNarrow ? 24 : 32),
|
||||||
|
|
||||||
|
// Login Button
|
||||||
|
Obx(() => ElevatedButton(
|
||||||
|
onPressed: controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: controller.login,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: isNarrow ? 16 : 18,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
backgroundColor: controller.isLoading.value
|
||||||
|
? Get.theme.disabledColor
|
||||||
|
: Get.theme.colorScheme.primary,
|
||||||
|
foregroundColor:
|
||||||
|
Get.theme.colorScheme.onPrimary,
|
||||||
|
elevation: controller.isLoading.value ? 0 : 4,
|
||||||
|
shadowColor: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.4),
|
||||||
|
),
|
||||||
|
child: controller.isLoading.value
|
||||||
|
? SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
valueColor:
|
||||||
|
AlwaysStoppedAnimation<Color>(Get
|
||||||
|
.theme.colorScheme.onPrimary),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'login'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:gopeed/api/api.dart';
|
|
||||||
|
import '../../../../util/updater.dart';
|
||||||
|
|
||||||
class SettingController extends GetxController {
|
class SettingController extends GetxController {
|
||||||
final tapStatues = <String, bool>{}.obs;
|
final tapStatues = <String, bool>{}.obs;
|
||||||
final latestVersion = "".obs;
|
final latestVersion = Rxn<VersionInfo>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@@ -26,23 +25,6 @@ class SettingController extends GetxController {
|
|||||||
|
|
||||||
// fetch latest version
|
// fetch latest version
|
||||||
void fetchLatestVersion() async {
|
void fetchLatestVersion() async {
|
||||||
String? releaseDataStr;
|
latestVersion.value = await checkUpdate();
|
||||||
try {
|
|
||||||
releaseDataStr = (await proxyRequest(
|
|
||||||
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
|
||||||
.data;
|
|
||||||
} catch (e) {
|
|
||||||
releaseDataStr =
|
|
||||||
(await proxyRequest("https://gopeed.com/api/release")).data;
|
|
||||||
}
|
|
||||||
if (releaseDataStr == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final releaseData = jsonDecode(releaseDataStr);
|
|
||||||
final tagName = releaseData["tag_name"];
|
|
||||||
if (tagName == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
latestVersion.value = releaseData["tag_name"].substring(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ import '../../../../util/log_util.dart';
|
|||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
import '../../../../util/scheme_register/scheme_register.dart';
|
import '../../../../util/scheme_register/scheme_register.dart';
|
||||||
|
import '../../../../util/updater.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../views/check_list_view.dart';
|
import '../../../views/check_list_view.dart';
|
||||||
import '../../../views/directory_selector.dart';
|
import '../../../views/directory_selector.dart';
|
||||||
@@ -514,29 +515,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildVersion() {
|
buildVersion() {
|
||||||
bool isNewVersion(String current, String latest) {
|
var hasNewVersion = controller.latestVersion.value != null;
|
||||||
if (latest == "") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final v1Parts = current.split('.');
|
|
||||||
final v2Parts = latest.split('.');
|
|
||||||
|
|
||||||
for (var i = 0; i < 3; i++) {
|
|
||||||
final v1Part = int.parse(v1Parts[i]);
|
|
||||||
final v2Part = int.parse(v2Parts[i]);
|
|
||||||
|
|
||||||
if (v1Part < v2Part) {
|
|
||||||
return true;
|
|
||||||
} else if (v1Part > v2Part) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasNewVersion =
|
|
||||||
isNewVersion(packageInfo.version, controller.latestVersion.value);
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: hasNewVersion
|
title: hasNewVersion
|
||||||
? badges.Badge(
|
? badges.Badge(
|
||||||
@@ -546,32 +525,31 @@ class SettingView extends GetView<SettingController> {
|
|||||||
subtitle: Text(packageInfo.version),
|
subtitle: Text(packageInfo.version),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (hasNewVersion) {
|
if (hasNewVersion) {
|
||||||
showDialog(
|
showUpdateDialog(context, controller.latestVersion.value!);
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
content: Text('newVersionTitle'.trParams(
|
|
||||||
{'version': controller.latestVersion.value})),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
child: Text('newVersionLater'.tr),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
launchUrl(Uri.parse('https://gopeed.com'),
|
|
||||||
mode: LaunchMode.externalApplication);
|
|
||||||
},
|
|
||||||
child: Text('newVersionUpdate'.tr),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final buildAutoCheckUpdate = _buildConfigItem(
|
||||||
|
'notifyWhenNewVersion',
|
||||||
|
() =>
|
||||||
|
downloaderCfg.value.extra.notifyWhenNewVersion ? 'on'.tr : 'off'.tr,
|
||||||
|
(Key key) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Switch(
|
||||||
|
value: downloaderCfg.value.extra.notifyWhenNewVersion,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
downloaderCfg.update((val) {
|
||||||
|
val!.extra.notifyWhenNewVersion = value;
|
||||||
|
});
|
||||||
|
await debounceSave();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
buildThanks() {
|
buildThanks() {
|
||||||
const thankPage =
|
const thankPage =
|
||||||
'https://github.com/GopeedLab/gopeed/graphs/contributors';
|
'https://github.com/GopeedLab/gopeed/graphs/contributors';
|
||||||
@@ -762,7 +740,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
width: 80,
|
width: 80,
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: startCfg.value.network,
|
value: startCfg.value.network,
|
||||||
onChanged: Util.isDesktop()
|
onChanged: Util.isDesktop() || Util.isAndroid()
|
||||||
? (value) async {
|
? (value) async {
|
||||||
startCfg.update((val) {
|
startCfg.update((val) {
|
||||||
val!.network = value!;
|
val!.network = value!;
|
||||||
@@ -786,7 +764,8 @@ class SettingView extends GetView<SettingController> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
if (Util.isDesktop() && startCfg.value.network == 'tcp') {
|
if ((Util.isDesktop() || Util.isAndroid()) &&
|
||||||
|
startCfg.value.network == 'tcp') {
|
||||||
final arr = startCfg.value.address.split(':');
|
final arr = startCfg.value.address.split(':');
|
||||||
var ip = '127.0.0.1';
|
var ip = '127.0.0.1';
|
||||||
var port = '0';
|
var port = '0';
|
||||||
@@ -997,6 +976,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
children: _addDivider([
|
children: _addDivider([
|
||||||
buildHomepage(),
|
buildHomepage(),
|
||||||
buildVersion(),
|
buildVersion(),
|
||||||
|
buildAutoCheckUpdate(),
|
||||||
buildThanks(),
|
buildThanks(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import '../modules/extension/bindings/extension_binding.dart';
|
|||||||
import '../modules/extension/views/extension_view.dart';
|
import '../modules/extension/views/extension_view.dart';
|
||||||
import '../modules/home/bindings/home_binding.dart';
|
import '../modules/home/bindings/home_binding.dart';
|
||||||
import '../modules/home/views/home_view.dart';
|
import '../modules/home/views/home_view.dart';
|
||||||
|
import '../modules/login/bindings/login_binding.dart';
|
||||||
|
import '../modules/login/views/login_view.dart';
|
||||||
import '../modules/redirect/bindings/redirect_binding.dart';
|
import '../modules/redirect/bindings/redirect_binding.dart';
|
||||||
import '../modules/redirect/views/redirect_view.dart';
|
import '../modules/redirect/views/redirect_view.dart';
|
||||||
import '../modules/root/bindings/root_binding.dart';
|
import '../modules/root/bindings/root_binding.dart';
|
||||||
@@ -63,6 +65,12 @@ class AppPages {
|
|||||||
binding: SettingBinding(),
|
binding: SettingBinding(),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
GetPage(
|
||||||
|
name: _Paths.LOGIN,
|
||||||
|
page: () => const LoginView(),
|
||||||
|
binding: LoginBinding(),
|
||||||
|
transition: Transition.fadeIn,
|
||||||
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: _Paths.CREATE,
|
name: _Paths.CREATE,
|
||||||
transition: Transition.downToUp,
|
transition: Transition.downToUp,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ abstract class Routes {
|
|||||||
static const ROOT = _Paths.ROOT;
|
static const ROOT = _Paths.ROOT;
|
||||||
static const HOME = _Paths.HOME;
|
static const HOME = _Paths.HOME;
|
||||||
static const CREATE = _Paths.CREATE;
|
static const CREATE = _Paths.CREATE;
|
||||||
|
static const LOGIN = _Paths.LOGIN;
|
||||||
static const TASK = _Paths.HOME + _Paths.TASK;
|
static const TASK = _Paths.HOME + _Paths.TASK;
|
||||||
static const TASK_FILES = TASK + _Paths.TASK_FILES;
|
static const TASK_FILES = TASK + _Paths.TASK_FILES;
|
||||||
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
|
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
|
||||||
@@ -20,6 +21,7 @@ abstract class _Paths {
|
|||||||
static const ROOT = '/';
|
static const ROOT = '/';
|
||||||
static const HOME = '/home';
|
static const HOME = '/home';
|
||||||
static const CREATE = '/create';
|
static const CREATE = '/create';
|
||||||
|
static const LOGIN = '/login';
|
||||||
static const TASK = '/task';
|
static const TASK = '/task';
|
||||||
static const TASK_FILES = '/files';
|
static const TASK_FILES = '/files';
|
||||||
static const EXTENSION = '/extension';
|
static const EXTENSION = '/extension';
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dart_ipc/dart_ipc.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import '../../util/util.dart';
|
||||||
|
|
||||||
|
/// RPC context class that contains request information and response methods
|
||||||
|
class RpcContext {
|
||||||
|
final HttpRequest request;
|
||||||
|
final HttpResponse response;
|
||||||
|
String? _bodyCache;
|
||||||
|
|
||||||
|
RpcContext(this.request, this.response);
|
||||||
|
|
||||||
|
/// Read and parse JSON request body
|
||||||
|
Future<Map<String, dynamic>> readJSON() async {
|
||||||
|
if (_bodyCache == null) {
|
||||||
|
final content = <int>[];
|
||||||
|
await for (var data in request) {
|
||||||
|
content.addAll(data);
|
||||||
|
}
|
||||||
|
_bodyCache = String.fromCharCodes(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_bodyCache!.isEmpty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonDecode(_bodyCache!) as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> readText() async {
|
||||||
|
if (_bodyCache == null) {
|
||||||
|
final content = <int>[];
|
||||||
|
await for (var data in request) {
|
||||||
|
content.addAll(data);
|
||||||
|
}
|
||||||
|
_bodyCache = String.fromCharCodes(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _bodyCache ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write JSON response
|
||||||
|
Future<void> writeJSON(Map<String, dynamic> data) async {
|
||||||
|
response.headers.contentType = ContentType.json;
|
||||||
|
response.write(jsonEncode(data));
|
||||||
|
await response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write error response
|
||||||
|
Future<void> writeError(String message, [int statusCode = 500]) async {
|
||||||
|
response.statusCode = statusCode;
|
||||||
|
await writeJSON({'error': message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route handler type definition
|
||||||
|
typedef RouteHandler = Future<void> Function(RpcContext ctx);
|
||||||
|
|
||||||
|
/// Route registry
|
||||||
|
class RouteRegistry {
|
||||||
|
final Map<String, RouteHandler> _routes = {};
|
||||||
|
|
||||||
|
/// Register a route
|
||||||
|
void register(String path, RouteHandler handler) {
|
||||||
|
_routes[path] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get route handler
|
||||||
|
RouteHandler? getHandler(String path) {
|
||||||
|
return _routes[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all registered routes
|
||||||
|
List<String> get routes => _routes.keys.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start RPC server
|
||||||
|
Future<void> startRpcServer([Map<String, RouteHandler>? routes]) async {
|
||||||
|
String path;
|
||||||
|
if (Util.isWindows()) {
|
||||||
|
path = r'\\.\pipe\gopeed_host';
|
||||||
|
} else {
|
||||||
|
path = await Util.homePathJoin("gopeed_host.sock");
|
||||||
|
// try to delete existing socket file
|
||||||
|
final socketFile = File(path);
|
||||||
|
if (await socketFile.exists()) {
|
||||||
|
try {
|
||||||
|
await socketFile.delete();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create route registry
|
||||||
|
final registry = RouteRegistry();
|
||||||
|
|
||||||
|
// Register provided routes
|
||||||
|
if (routes != null) {
|
||||||
|
routes.forEach((path, handler) {
|
||||||
|
registry.register(path, handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverSocket = await bind(path);
|
||||||
|
final httpServer = HttpServer.listenOn(serverSocket);
|
||||||
|
|
||||||
|
httpServer.forEach((HttpRequest request) async {
|
||||||
|
final ctx = RpcContext(request, request.response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get request path
|
||||||
|
final requestPath = request.uri.path;
|
||||||
|
// Find route handler
|
||||||
|
final handler = registry.getHandler(requestPath);
|
||||||
|
if (handler != null) {
|
||||||
|
// Execute route handler
|
||||||
|
await handler(ctx);
|
||||||
|
} else {
|
||||||
|
// 404 Route not found
|
||||||
|
await ctx.writeError('Route not found: $requestPath', 404);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await ctx.writeError('Internal server error: $e', 500);
|
||||||
|
} finally {
|
||||||
|
await request.response.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,10 +6,10 @@ import 'package:hive/hive.dart';
|
|||||||
import 'entity.dart';
|
import 'entity.dart';
|
||||||
|
|
||||||
const String _startConfig = 'startConfig';
|
const String _startConfig = 'startConfig';
|
||||||
const String _lastRunningConfig = 'lastRunningConfig';
|
|
||||||
const String _windowState = 'windowState';
|
const String _windowState = 'windowState';
|
||||||
const String _bookmark = 'bookmark';
|
const String _bookmark = 'bookmark';
|
||||||
const String _createHistory = 'createHistory';
|
const String _createHistory = 'createHistory';
|
||||||
|
const String _webToken = 'webToken';
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
static final Database _instance = Database._internal();
|
static final Database _instance = Database._internal();
|
||||||
@@ -82,6 +82,14 @@ class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void saveWebToken(String token) {
|
||||||
|
save<String>(_webToken, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getWebToken() {
|
||||||
|
return get<String>(_webToken, (json) => json.toString());
|
||||||
|
}
|
||||||
|
|
||||||
void saveCreateHistory(String url) {
|
void saveCreateHistory(String url) {
|
||||||
var list = getCreateHistory() ?? [];
|
var list = getCreateHistory() ?? [];
|
||||||
list.remove(url);
|
list.remove(url);
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
const deDE = {
|
||||||
|
'de_DE': {
|
||||||
|
'label': 'Deutsch',
|
||||||
|
'error': 'Fehler',
|
||||||
|
'tip': 'Tipp',
|
||||||
|
'confirm': 'Bestätigen',
|
||||||
|
'cancel': 'Abbrechen',
|
||||||
|
'on': 'An',
|
||||||
|
'off': 'Aus',
|
||||||
|
'selectAll': 'Alle Auswählen',
|
||||||
|
'select': 'Auswählen',
|
||||||
|
'task': 'Aufgaben',
|
||||||
|
'downloading': 'Wird Heruntergeladen',
|
||||||
|
'downloaded': 'Heruntergeladen',
|
||||||
|
'setting': 'Einstellungen',
|
||||||
|
'donate': 'Spenden',
|
||||||
|
'exit': 'Beenden',
|
||||||
|
'create': 'Aufgabe Erstellen',
|
||||||
|
'directDownload': 'Direkter Download',
|
||||||
|
'advancedOptions': 'Erweiterte Einstellungen',
|
||||||
|
'followSettings': 'Einstellungen Folgen',
|
||||||
|
'downloadLink': 'Download Link',
|
||||||
|
'downloadLinkValid': 'Bitte geben Sie den Download-Link ein',
|
||||||
|
'downloadLinkHit': 'Bitte geben Sie den Download-Link ein, einen pro Zeile@append',
|
||||||
|
'downloadLinkHitDesktop': ', oder ziehen Sie die Torrent-Datei direkt hierher',
|
||||||
|
'download': 'Download',
|
||||||
|
'noFileSelected': 'Bitte wählen Sie mindestens eine Datei aus, um fortzufahren.',
|
||||||
|
'noStoragePermission': 'Speicherberechtigung erforderlich',
|
||||||
|
'selectFile': 'Datei auswählen',
|
||||||
|
'rename': 'Umbenennen',
|
||||||
|
'basic': 'Basic',
|
||||||
|
'advanced': 'Fortschrittlich',
|
||||||
|
'general': 'Allgemein',
|
||||||
|
'downloadDir': 'Download-Verzeichnis',
|
||||||
|
'downloadDirValid': 'Bitte wählen Sie das Download-Verzeichnis',
|
||||||
|
'connections': 'Anschlüsse',
|
||||||
|
'useServerCtime': 'Serverzeit für die Dateierstellung verwenden',
|
||||||
|
'maxRunning': 'Maximale Anzahl ausgeführter Aufgaben',
|
||||||
|
'defaultDirectDownload': 'Direkten Download standardmäßig aktivieren',
|
||||||
|
'items': '@count Artikel',
|
||||||
|
'subscribeTracker': 'Abonnement-Tracker',
|
||||||
|
'subscribeFail':
|
||||||
|
'Anmeldung fehlgeschlagen. Bitte überprüfen Sie das Netzwerk oder versuchen Sie es später erneut.',
|
||||||
|
'update': 'Aktualisieren',
|
||||||
|
'updateDaily': 'Tägliche Aktualisierung',
|
||||||
|
'lastUpdate': 'Letzte Aktualisierung: @time',
|
||||||
|
'addTracker': 'Tracker hinzufügen',
|
||||||
|
'addTrackerHit': 'Bitte geben Sie die URL des Tracker-Servers ein, eine pro Zeile',
|
||||||
|
'ui': 'Benutzeroberfläche',
|
||||||
|
'theme': 'Thema',
|
||||||
|
'themeSystem': 'System',
|
||||||
|
'themeLight': 'Lichtmodus',
|
||||||
|
'themeDark': 'Dunkler Modus',
|
||||||
|
'locale': 'Sprache',
|
||||||
|
'about': 'Über',
|
||||||
|
'homepage': 'Homepage',
|
||||||
|
'version': 'Version',
|
||||||
|
'protocol': 'Protokoll',
|
||||||
|
'port': 'Port',
|
||||||
|
'apiToken': 'API Token',
|
||||||
|
'notSet': 'NS',
|
||||||
|
'set': 'SET',
|
||||||
|
'portInUse': 'Port [@port] wird verwendet, bitte ändern Sie den Port',
|
||||||
|
'effectAfterRestart': 'Wirkung nach Neustart',
|
||||||
|
'developer': 'Entwickler-in',
|
||||||
|
'logDirectory': 'Protokollverzeichnis',
|
||||||
|
'show': 'Zeigen',
|
||||||
|
'continue': 'Weitermachen',
|
||||||
|
'pause': 'Pause',
|
||||||
|
'startAll': 'Alles starten',
|
||||||
|
'pauseAll': 'Alle pausieren',
|
||||||
|
'deleteTask': '@count Aufgaben löschen',
|
||||||
|
'deleteTaskTip': 'Heruntergeladene Dateien behalten',
|
||||||
|
'delete': 'Löschen',
|
||||||
|
'newVersionTitle': 'Entdecken Sie die neue Version @version',
|
||||||
|
'newVersionUpdate': 'Jetzt aktualisieren',
|
||||||
|
'newVersionLater': 'Später',
|
||||||
|
'extensions': 'Erweiterungen',
|
||||||
|
'extensionInstallUrl': 'Installations-URL',
|
||||||
|
'extensionInstallSuccess': 'Erfolgreich installiert',
|
||||||
|
'extensionUpdateSuccess': 'Erfolgreich aktualisiert',
|
||||||
|
'extensionDelete': 'Erweiterung löschen',
|
||||||
|
'extensionAlreadyLatest': 'Es ist bereits die neueste Version',
|
||||||
|
'extensionFind': 'Erweiterungen suchen',
|
||||||
|
'extensionDevelop': 'Erweiterungen entwickeln',
|
||||||
|
'history': 'Verlauf',
|
||||||
|
'clearHistory': 'Verlauf löschen',
|
||||||
|
'noHistoryFound': 'Kein Verlauf gefunden',
|
||||||
|
'serviceTitle': 'Download-Service',
|
||||||
|
'serviceText': 'Läuft',
|
||||||
|
'network': 'Netzwerk',
|
||||||
|
'proxy': 'Proxy',
|
||||||
|
'noProxy': 'Kein Proxy',
|
||||||
|
'systemProxy': 'System-Proxy',
|
||||||
|
'customProxy': 'Benutzerdefinierter Proxy',
|
||||||
|
'server': 'Server',
|
||||||
|
'username': 'Benutzername',
|
||||||
|
'password': 'Passwort',
|
||||||
|
'thanks': 'Danke',
|
||||||
|
'thanksDesc':
|
||||||
|
'Vielen Dank an alle Mitwirkenden, die beim Aufbau und der Entwicklung der Gopeed-Community geholfen haben!',
|
||||||
|
'browserExtension': 'Browsererweiterung',
|
||||||
|
'launchAtStartup': 'Beim Start starten',
|
||||||
|
'seedConfig': 'Seed-Konfiguration',
|
||||||
|
'seedKeep': 'Weiter-seeden bis manuell gestoppt',
|
||||||
|
'seedRatio': 'Seed Verhältniss',
|
||||||
|
'seedTime': 'Seed-Zeit (Minuten)',
|
||||||
|
'setAsDefaultBtClient': 'Als Standard-BT-Client festlegen',
|
||||||
|
'taskDetail': 'Aufgabendetails',
|
||||||
|
'taskName': 'Aufgabenname',
|
||||||
|
'taskUrl': 'Aufgaben-URL',
|
||||||
|
'downloadPath': 'Download-Pfad',
|
||||||
|
'skipVerifyCert': 'Zertifikatsüberprüfung überspringen',
|
||||||
|
'name': 'Name',
|
||||||
|
'size': 'Größe',
|
||||||
|
'unknown': 'Unbekannt',
|
||||||
|
'fileSelectedCount': 'Dateien: ',
|
||||||
|
'fileSelectedSize': 'Größe: ',
|
||||||
|
'httpHeaderName': 'Header-Name',
|
||||||
|
'httpHeaderValue': 'Header-Wert',
|
||||||
|
'login': 'Login',
|
||||||
|
'username_required': 'Bitte geben Sie Ihren Benutzernamen ein',
|
||||||
|
'password_required': 'Bitte geben Sie Ihr Passwort ein',
|
||||||
|
'login_success': 'Anmeldung erfolgreich',
|
||||||
|
'login_failed': 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort',
|
||||||
|
'login_failed_network':
|
||||||
|
'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihre Netzwerkverbindung',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -52,6 +52,7 @@ const enUS = {
|
|||||||
'themeLight': 'Light',
|
'themeLight': 'Light',
|
||||||
'themeDark': 'Dark',
|
'themeDark': 'Dark',
|
||||||
'locale': 'Language',
|
'locale': 'Language',
|
||||||
|
'notifyWhenNewVersion': 'Notify for updates',
|
||||||
'about': 'About',
|
'about': 'About',
|
||||||
'homepage': 'Homepage',
|
'homepage': 'Homepage',
|
||||||
'version': 'Version',
|
'version': 'Version',
|
||||||
@@ -118,5 +119,12 @@ const enUS = {
|
|||||||
'fileSelectedSize': 'Size: ',
|
'fileSelectedSize': 'Size: ',
|
||||||
'httpHeaderName': 'Header Name',
|
'httpHeaderName': 'Header Name',
|
||||||
'httpHeaderValue': 'Header Value',
|
'httpHeaderValue': 'Header Value',
|
||||||
|
'login': 'Login',
|
||||||
|
'username_required': 'Please enter your username',
|
||||||
|
'password_required': 'Please enter your password',
|
||||||
|
'login_success': 'Login successful',
|
||||||
|
'login_failed': 'Login failed, please check your username and password',
|
||||||
|
'login_failed_network':
|
||||||
|
'Login failed, please check your network connection',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
const huHU = {
|
||||||
|
'hu_HU': {
|
||||||
|
'label': 'Magyar',
|
||||||
|
'error': 'Hiba',
|
||||||
|
'tip': 'Tipp',
|
||||||
|
'confirm': 'Megerősít',
|
||||||
|
'cancel': 'Mégsem',
|
||||||
|
'on': 'Bekapcsolva',
|
||||||
|
'off': 'Kikapcsolva',
|
||||||
|
'selectAll': 'Összes kiválasztása',
|
||||||
|
'select': 'Kiválaszt',
|
||||||
|
'task': 'Feladatok',
|
||||||
|
'downloading': 'letöltés alatt',
|
||||||
|
'downloaded': 'letöltve',
|
||||||
|
'setting': 'Beállítások',
|
||||||
|
'donate': 'Támogatás',
|
||||||
|
'exit': 'Kilépés',
|
||||||
|
'create': 'Feladat létrehozása',
|
||||||
|
'directDownload': 'Közvetlen letöltés',
|
||||||
|
'advancedOptions': 'Haladó beállítások',
|
||||||
|
'followSettings': 'Beállítások követése',
|
||||||
|
'downloadLink': 'Letöltési hivatkozás',
|
||||||
|
'downloadLinkValid': 'Kérjük, adja meg a letöltési hivatkozást',
|
||||||
|
'downloadLinkHit': 'Kérjük, adja meg a letöltési hivatkozást, soronként egyet@append',
|
||||||
|
'downloadLinkHitDesktop': ', vagy húzza ide a torrent fájlt közvetlenül',
|
||||||
|
'download': 'Letöltés',
|
||||||
|
'noFileSelected': 'Kérjük, válasszon ki legalább egy fájlt a folytatáshoz.',
|
||||||
|
'noStoragePermission': 'Tárolási engedély szükséges',
|
||||||
|
'selectFile': 'Fájl kiválasztása',
|
||||||
|
'rename': 'Átnevezés',
|
||||||
|
'basic': 'Alap',
|
||||||
|
'advanced': 'Haladó',
|
||||||
|
'general': 'Általános',
|
||||||
|
'downloadDir': 'Letöltési könyvtár',
|
||||||
|
'downloadDirValid': 'Kérjük, válassza ki a letöltési könyvtárat',
|
||||||
|
'connections': 'Kapcsolatok',
|
||||||
|
'useServerCtime': 'Használja a szerveridőt a fájl létrehozásához',
|
||||||
|
'maxRunning': 'Maximális futó feladatok',
|
||||||
|
'defaultDirectDownload': 'Alapértelmezettként ellenőrizze a közvetlen letöltést',
|
||||||
|
'items': '@count elem',
|
||||||
|
'subscribeTracker': 'Nyomkövető előfizetése',
|
||||||
|
'subscribeFail':
|
||||||
|
'Az előfizetés nem sikerült, kérjük, ellenőrizze a hálózatot, vagy próbálja újra később',
|
||||||
|
'update': 'Frissítés',
|
||||||
|
'updateDaily': 'Napi frissítés',
|
||||||
|
'lastUpdate': 'Utolsó frissítés: @time',
|
||||||
|
'addTracker': 'Nyomkövető hozzáadása',
|
||||||
|
'addTrackerHit': 'Kérjük, adja meg a nyomkövető szerver URL-jét, soronként egyet',
|
||||||
|
'ui': 'Felhasználói felület',
|
||||||
|
'theme': 'Téma',
|
||||||
|
'themeSystem': 'Rendszer',
|
||||||
|
'themeLight': 'Világos',
|
||||||
|
'themeDark': 'Sötét',
|
||||||
|
'locale': 'Nyelv',
|
||||||
|
'about': 'Névjegy',
|
||||||
|
'homepage': 'Főoldal',
|
||||||
|
'version': 'Verzió',
|
||||||
|
'protocol': 'Protokoll',
|
||||||
|
'port': 'Port',
|
||||||
|
'apiToken': 'API token',
|
||||||
|
'notSet': 'NS',
|
||||||
|
'set': 'BEÁLLÍT',
|
||||||
|
'portInUse': 'A [@port] port használatban van, kérjük, változtassa meg a portot',
|
||||||
|
'effectAfterRestart': 'Kihatás indítás után',
|
||||||
|
'developer': 'Fejlesztő',
|
||||||
|
'logDirectory': 'Napló könyvtár',
|
||||||
|
'show': 'Megjelenítés',
|
||||||
|
'continue': 'Folyatás',
|
||||||
|
'pause': 'Szünet',
|
||||||
|
'startAll': 'Összes indítása',
|
||||||
|
'pauseAll': 'Összes szüneteltetése',
|
||||||
|
'deleteTask': '@count feladat törlése',
|
||||||
|
'deleteTaskTip': 'Letöltött fájlok megőrzése',
|
||||||
|
'delete': 'Törlés',
|
||||||
|
'newVersionTitle': 'Új verzió felfedezése @version',
|
||||||
|
'newVersionUpdate': 'Frissítés most',
|
||||||
|
'newVersionLater': 'Később',
|
||||||
|
'extensions': 'Bővítmények',
|
||||||
|
'extensionInstallUrl': 'Telepítési URL',
|
||||||
|
'extensionInstallSuccess': 'Sikeresen telepítve',
|
||||||
|
'extensionUpdateSuccess': 'Sikeresen frissítve',
|
||||||
|
'extensionDelete': 'Bővítmény törlése',
|
||||||
|
'extensionAlreadyLatest': 'Ez már a legfrissebb verzió',
|
||||||
|
'extensionFind': 'Bővítmények keresése',
|
||||||
|
'extensionDevelop': 'Bővítmények fejlesztése',
|
||||||
|
'history': 'Előzmények',
|
||||||
|
'clearHistory': 'Előzmények törlése',
|
||||||
|
'noHistoryFound': 'Nincs előzménye',
|
||||||
|
'serviceTitle': 'Letöltési szolgáltatás',
|
||||||
|
'serviceText': 'Futtatás',
|
||||||
|
'network': 'Hálózat',
|
||||||
|
'proxy': 'Proxy',
|
||||||
|
'noProxy': 'Nincs proxy',
|
||||||
|
'systemProxy': 'Rendszer proxy',
|
||||||
|
'customProxy': 'Egyéni proxy',
|
||||||
|
'server': 'Szerver',
|
||||||
|
'username': 'Felhasználónév',
|
||||||
|
'password': 'Jelszó',
|
||||||
|
'thanks': 'Köszönet',
|
||||||
|
'thanksDesc':
|
||||||
|
'Köszönet minden hozzájárulónak, aki segített a Gopeed közösség építésében és fejlesztésében!',
|
||||||
|
'browserExtension': 'Böngésző bővítmény',
|
||||||
|
'launchAtStartup': 'Indítás indításkor',
|
||||||
|
'seedConfig': 'Seed beállítás',
|
||||||
|
'seedKeep': 'Tartsa a seedet amíg manuálisan le nem állítják',
|
||||||
|
'seedRatio': 'Seed arány',
|
||||||
|
'seedTime': 'Seed idő (perc)',
|
||||||
|
'setAsDefaultBtClient': 'Alapértelmezett BT klienseként beállít',
|
||||||
|
'taskDetail': 'Feladat részlete',
|
||||||
|
'taskName': 'Feladat neve',
|
||||||
|
'taskUrl': 'Feladat URL',
|
||||||
|
'downloadPath': 'Letöltési útvonal',
|
||||||
|
'skipVerifyCert': 'Tanúsítvány ellenőrzés átugrása',
|
||||||
|
'name': 'Név',
|
||||||
|
'size': 'Méret',
|
||||||
|
'unknown': 'Ismeretlen',
|
||||||
|
'fileSelectedCount': 'Fájlok: ',
|
||||||
|
'fileSelectedSize': 'Méret: ',
|
||||||
|
'httpHeaderName': 'Fejléc neve',
|
||||||
|
'httpHeaderValue': 'Fejléc értéke',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -7,43 +7,44 @@ const trTR = {
|
|||||||
'cancel': 'İptal',
|
'cancel': 'İptal',
|
||||||
'on': 'Açık',
|
'on': 'Açık',
|
||||||
'off': 'Kapalı',
|
'off': 'Kapalı',
|
||||||
'selectAll': 'Hepsini seç',
|
'selectAll': 'Tümünü Seç',
|
||||||
|
'select': 'Seç',
|
||||||
'task': 'Görevler',
|
'task': 'Görevler',
|
||||||
'downloading': 'indiriliyor',
|
'downloading': 'İndiriliyor',
|
||||||
'downloaded': 'indirildi',
|
'downloaded': 'İndirildi',
|
||||||
'setting': 'Ayarlar',
|
'setting': 'Ayarlar',
|
||||||
'donate': 'Bağış',
|
'donate': 'Bağış Yap',
|
||||||
'exit': 'Çık',
|
'exit': 'Çıkış',
|
||||||
'create': 'Görev oluştur',
|
'create': 'Görev Oluştur',
|
||||||
'directDownload': 'Doğrudan indir',
|
'directDownload': 'Doğrudan İndirme',
|
||||||
'advancedOptions': 'Gelişmiş ayarlar',
|
'advancedOptions': 'Gelişmiş Seçenekler',
|
||||||
'followSettings': 'Ayarları takip et',
|
'followSettings': 'Ayarları Takip Et',
|
||||||
'downloadLink': 'İndirme bağlantısı',
|
'downloadLink': 'İndirme Bağlantısı',
|
||||||
'downloadLinkValid': 'Lütfen indirme bağlantısını girin',
|
'downloadLinkValid': 'Lütfen indirme bağlantısını girin',
|
||||||
'downloadLinkHit': 'Lütfen indirme bağlantısını girin, HTTP/HTTPS/MAGNET desteklenir@append',
|
'downloadLinkHit': 'Lütfen her satıra bir indirme bağlantısı girin@append',
|
||||||
'downloadLinkHitDesktop': ', veya torrent dosyasını doğrudan buraya sürükleyin',
|
'downloadLinkHitDesktop': ', veya doğrudan torrent dosyasını buraya sürükleyin',
|
||||||
'download': 'İndir',
|
'download': 'İndir',
|
||||||
'noFileSelected': 'Devam etmek için lütfen en az bir dosya seçin.',
|
'noFileSelected': 'Devam etmek için en az bir dosya seçin.',
|
||||||
'noStoragePermission': 'Depolama izni gerekli',
|
'noStoragePermission': 'Depolama izni gerekli',
|
||||||
'selectFile': 'Dosya seç',
|
'selectFile': 'Dosya Seç',
|
||||||
'rename': 'Yeniden adlandır',
|
'rename': 'Yeniden Adlandır',
|
||||||
'basic': 'Temel',
|
'basic': 'Temel',
|
||||||
'advanced': 'Gelişmiş',
|
'advanced': 'Gelişmiş',
|
||||||
'general': 'Genel',
|
'general': 'Genel',
|
||||||
'downloadDir': 'İndirme dizini',
|
'downloadDir': 'İndirme Dizini',
|
||||||
'downloadDirValid': 'Lütfen indirme dizinini seçin',
|
'downloadDirValid': 'Lütfen bir indirme dizini seçin',
|
||||||
'connections': 'Bağlantılar',
|
'connections': 'Bağlantılar',
|
||||||
'useServerCtime': 'Dosya oluşturma için sunucu saatini kullan',
|
'useServerCtime': 'Dosya oluşturma için sunucu zamanını kullan',
|
||||||
'maxRunning': 'Maksimum çalışan görev sayısı',
|
'maxRunning': 'Maksimum Çalışan Görev',
|
||||||
'defaultDirectDownload': 'Doğrudan indir seçili olsun',
|
'defaultDirectDownload': 'Varsayılan olarak doğrudan indirmeyi seç',
|
||||||
'items': '@count tane',
|
'items': '@count öğe',
|
||||||
'subscribeTracker': 'İzleyiciye abone ol',
|
'subscribeTracker': 'Tracker Aboneliği',
|
||||||
'subscribeFail': 'Abone olunamadı, lütfen ağı kontrol edin veya daha sonra tekrar deneyin',
|
'subscribeFail': 'Abonelik başarısız oldu, lütfen ağı kontrol edin veya daha sonra tekrar deneyin',
|
||||||
'update': 'Güncelle',
|
'update': 'Güncelle',
|
||||||
'updateDaily': 'Günlük olarak güncelle',
|
'updateDaily': 'Günlük güncelle',
|
||||||
'lastUpdate': 'Son güncellenme tarihi: @time',
|
'lastUpdate': 'Son güncelleme: @time',
|
||||||
'addTracker': 'İzleyici ekle',
|
'addTracker': 'Tracker Ekle',
|
||||||
'addTrackerHit': 'Lütfen her satıra bir tane olmak üzere izleyici sunucusu bağlantısını girin',
|
'addTrackerHit': 'Lütfen tracker sunucu URL’sini girin, her satıra bir tane',
|
||||||
'ui': 'Arayüz',
|
'ui': 'Arayüz',
|
||||||
'theme': 'Tema',
|
'theme': 'Tema',
|
||||||
'themeSystem': 'Sistem',
|
'themeSystem': 'Sistem',
|
||||||
@@ -51,59 +52,70 @@ const trTR = {
|
|||||||
'themeDark': 'Koyu',
|
'themeDark': 'Koyu',
|
||||||
'locale': 'Dil',
|
'locale': 'Dil',
|
||||||
'about': 'Hakkında',
|
'about': 'Hakkında',
|
||||||
'homepage': 'Anasayfa',
|
'homepage': 'Ana Sayfa',
|
||||||
'version': 'Sürüm',
|
'version': 'Sürüm',
|
||||||
'protocol': 'Protokol',
|
'protocol': 'Protokol',
|
||||||
'port': 'Bağlantı noktası',
|
'port': 'Bağlantı Noktası',
|
||||||
'apiToken': 'API anahtarı',
|
'apiToken': 'API Anahtarı',
|
||||||
'notSet': 'AYARLI DEĞİL',
|
'notSet': 'AYARLANMADI',
|
||||||
'set': 'AYARLI',
|
'set': 'AYARLA',
|
||||||
'portInUse': 'Bağlantı noktası [@port] kullanımda, lütfen bağlantı noktasını değiştirin',
|
'portInUse': 'Bağlantı noktası [@port] kullanımda, lütfen değiştirin',
|
||||||
'effectAfterRestart': 'Yeniden başlatma sonrası etki edecektir',
|
'effectAfterRestart': 'Yeniden başlatmadan sonra etkili olacak',
|
||||||
'developer': 'Geliştirici',
|
'developer': 'Geliştirici',
|
||||||
'logDirectory': 'Kayıt dizini',
|
'logDirectory': 'Günlük Dizini',
|
||||||
'show': 'Göster',
|
'show': 'Göster',
|
||||||
'startAll': 'Hepsini başlat',
|
'continue': 'Devam Et',
|
||||||
'pauseAll': 'Hepsini durdur',
|
'pause': 'Duraklat',
|
||||||
|
'startAll': 'Tümünü Başlat',
|
||||||
|
'pauseAll': 'Tümünü Duraklat',
|
||||||
'deleteTask': '@count görevi sil',
|
'deleteTask': '@count görevi sil',
|
||||||
'deleteTaskTip': 'İndirilen dosyaları sakla',
|
'deleteTaskTip': 'İndirilen dosyaları sakla',
|
||||||
'delete': 'Sil',
|
'delete': 'Sil',
|
||||||
'newVersionTitle': 'Yeni sürümü keşfet @version',
|
'newVersionTitle': 'Yeni sürüm bulundu @version',
|
||||||
'newVersionUpdate': 'Şimdi güncelle',
|
'newVersionUpdate': 'Şimdi Güncelle',
|
||||||
'newVersionLater': 'Sonra',
|
'newVersionLater': 'Daha Sonra',
|
||||||
'extensions': 'Uzantılar',
|
'extensions': 'Eklentiler',
|
||||||
'extensionInstallUrl': 'İndirme bağlantısı',
|
'extensionInstallUrl': 'Kurulum URL’si',
|
||||||
'extensionInstallSuccess': 'Başarıyla indirildi',
|
'extensionInstallSuccess': 'Başarıyla yüklendi',
|
||||||
'extensionUpdateSuccess': 'Başarıyla güncellendi',
|
'extensionUpdateSuccess': 'Başarıyla güncellendi',
|
||||||
'extensionDelete': 'Uzantıyı sil',
|
'extensionDelete': 'Eklentiyi Sil',
|
||||||
'extensionAlreadyLatest': 'Bu zaten en son sürüm',
|
'extensionAlreadyLatest': 'Zaten en son sürüm',
|
||||||
'extensionFind': 'Uzantı ara',
|
'extensionFind': 'Eklentileri Bul',
|
||||||
'extensionDevelop': 'Uzantı geliştir',
|
'extensionDevelop': 'Eklenti Geliştir',
|
||||||
'history': 'Geçmiş',
|
'history': 'Geçmiş',
|
||||||
'clearHistory': 'Geçmişi temizle',
|
'clearHistory': 'Geçmişi Temizle',
|
||||||
'noHistoryFound': 'Geçmiş bulunamadı',
|
'noHistoryFound': 'Geçmiş Bulunamadı',
|
||||||
'serviceTitle': 'Hizmet indir',
|
'serviceTitle': 'İndirme Servisi',
|
||||||
'serviceText': 'Çalışıyor',
|
'serviceText': 'Çalışıyor',
|
||||||
'network': 'Ağ',
|
'network': 'Ağ',
|
||||||
'proxy': 'Vekil',
|
'proxy': 'Vekil Sunucu',
|
||||||
'noProxy': 'Vekil yok',
|
'noProxy': 'Vekil Sunucu Yok',
|
||||||
'systemProxy': 'Sistem vekili',
|
'systemProxy': 'Sistem Vekil Sunucusu',
|
||||||
'customProxy': 'Özel vekil',
|
'customProxy': 'Özel Vekil Sunucu',
|
||||||
'server': 'Sunucu',
|
'server': 'Sunucu',
|
||||||
'username': 'Kullanıcı adı',
|
'username': 'Kullanıcı Adı',
|
||||||
'password': 'Şifre',
|
'password': 'Şifre',
|
||||||
'thanks': 'Teşekkürler',
|
'thanks': 'Teşekkürler',
|
||||||
'thanksDesc': 'Gopeed topluluğunun oluşmasına ve gelişmesine yardımcı olan tüm katılımcılara teşekkürler!',
|
'thanksDesc':
|
||||||
'browserExtension': 'Tarayıcı uzantısı',
|
'Gopeed topluluğunun inşasında ve geliştirilmesinde katkıda bulunan herkese teşekkürler!',
|
||||||
'launchAtStartup': 'Başlangıçta başlat',
|
'browserExtension': 'Tarayıcı Eklentisi',
|
||||||
'seedConfig': 'Seed ayarları',
|
'launchAtStartup': 'Başlangıçta Çalıştır',
|
||||||
'seedKeep': 'Manuel durdurulana kadar seed etmeye devam et',
|
'seedConfig': 'Seed Ayarları',
|
||||||
|
'seedKeep': 'Manuel olarak durdurulana kadar seed etmeye devam et',
|
||||||
'seedRatio': 'Seed oranı',
|
'seedRatio': 'Seed oranı',
|
||||||
'seedTime': 'Seed süresi (dakika)',
|
'seedTime': 'Seed süresi (dakika)',
|
||||||
'taskDetail': 'Görev detayı',
|
'setAsDefaultBtClient': 'Varsayılan BT istemcisi olarak ayarla',
|
||||||
'taskName': 'Görev adı',
|
'taskDetail': 'Görev Detayı',
|
||||||
'taskUrl': 'Görev URL',
|
'taskName': 'Görev Adı',
|
||||||
'downloadPath': 'İndirme yolu',
|
'taskUrl': 'Görev URL’si',
|
||||||
'skipVerifyCert': 'Sertifika doğrulamasını atla',
|
'downloadPath': 'İndirme Yolu',
|
||||||
|
'skipVerifyCert': 'Sertifika Doğrulamayı Atla',
|
||||||
|
'name': 'Ad',
|
||||||
|
'size': 'Boyut',
|
||||||
|
'unknown': 'Bilinmeyen',
|
||||||
|
'fileSelectedCount': 'Dosya: ',
|
||||||
|
'fileSelectedSize': 'Boyut: ',
|
||||||
|
'httpHeaderName': 'Başlık Adı',
|
||||||
|
'httpHeaderValue': 'Başlık Değeri',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
const ukUA = {
|
||||||
|
'uk_UA': {
|
||||||
|
'label': 'Мова',
|
||||||
|
'error': 'Помилка',
|
||||||
|
'tip': 'Підказка',
|
||||||
|
'confirm': 'Підтвердити',
|
||||||
|
'cancel': 'Скасувати',
|
||||||
|
'on': 'Увімк.',
|
||||||
|
'off': 'Вимк.',
|
||||||
|
'selectAll': 'Вибрати все',
|
||||||
|
'select': 'Вибрати',
|
||||||
|
'task': 'Завдання',
|
||||||
|
'downloading': 'завантажується',
|
||||||
|
'downloaded': 'завантажено',
|
||||||
|
'setting': 'Налаштування',
|
||||||
|
'donate': 'Підтримати',
|
||||||
|
'exit': 'Вихід',
|
||||||
|
'create': 'Створити завдання',
|
||||||
|
'directDownload': 'Пряме завантаження',
|
||||||
|
'advancedOptions': 'Додаткові параметри',
|
||||||
|
'followSettings': 'За замовчуванням',
|
||||||
|
'downloadLink': 'Посилання на завантаження',
|
||||||
|
'downloadLinkValid': 'Будь ласка, введіть посилання на завантаження',
|
||||||
|
'downloadLinkHit':
|
||||||
|
'Будь ласка, введіть посилання на завантаження, одне на рядок@Append',
|
||||||
|
'downloadLinkHitDesktop': ', або перетягніть сюди файл торенту',
|
||||||
|
'download': 'Завантажити',
|
||||||
|
'noFileSelected': 'Будь ласка, виберіть хоча б один файл для продовження.',
|
||||||
|
'noStoragePermission': 'Потрібен дозвіл на зберігання',
|
||||||
|
'selectFile': 'Вибрати файл',
|
||||||
|
'rename': 'Перейменувати',
|
||||||
|
'basic': 'Основні',
|
||||||
|
'advanced': 'Розширені',
|
||||||
|
'general': 'Загальні',
|
||||||
|
'downloadDir': 'Каталог завантажень',
|
||||||
|
'downloadDirValid': 'Будь ласка, виберіть каталог завантажень',
|
||||||
|
'connections': 'З’єднання',
|
||||||
|
'useServerCtime': 'Використовувати час сервера для створення файлу',
|
||||||
|
'maxRunning': 'Макс. завдань у черзі',
|
||||||
|
'defaultDirectDownload': 'Пряме завантаження за замовчуванням',
|
||||||
|
'items': '@count елементів',
|
||||||
|
'subscribeTracker': 'Підписатися на трекер',
|
||||||
|
'subscribeFail':
|
||||||
|
'Не вдалося підписатися, перевірте мережу або спробуйте пізніше',
|
||||||
|
'update': 'Оновити',
|
||||||
|
'updateDaily': 'Оновлювати щодня',
|
||||||
|
'lastUpdate': 'Останнє оновлення: @time',
|
||||||
|
'addTracker': 'Додати трекер',
|
||||||
|
'addTrackerHit': 'Будь ласка, введіть URL-адресу трекера, одну на рядок',
|
||||||
|
'ui': 'Інтерфейс',
|
||||||
|
'theme': 'Тема',
|
||||||
|
'themeSystem': 'Системна',
|
||||||
|
'themeLight': 'Світла',
|
||||||
|
'themeDark': 'Темна',
|
||||||
|
'locale': 'Мова',
|
||||||
|
'about': 'Про програму',
|
||||||
|
'homepage': 'Домашня сторінка',
|
||||||
|
'version': 'Версія',
|
||||||
|
'protocol': 'Протокол',
|
||||||
|
'port': 'Порт',
|
||||||
|
'apiToken': 'API Токен',
|
||||||
|
'notSet': 'Не задано',
|
||||||
|
'set': 'Задати',
|
||||||
|
'portInUse': 'Порт [@PORT] вже використовується, будь ласка, змініть порт',
|
||||||
|
'effectAfterRestart': 'Ефект після перезапуску',
|
||||||
|
'developer': 'Розробник',
|
||||||
|
'logDirectory': 'Каталог журналів',
|
||||||
|
'show': 'Показати',
|
||||||
|
'continue': 'Продовжити',
|
||||||
|
'pause': 'Пауза',
|
||||||
|
'startAll': 'Запустити все',
|
||||||
|
'pauseAll': 'Зупинити все',
|
||||||
|
'deleteTask': 'Видалити @count завдань',
|
||||||
|
'deleteTaskTip': 'Зберегти завантажені файли',
|
||||||
|
'delete': 'Видалити',
|
||||||
|
'newVersionTitle': 'Доступна нова версія @Version',
|
||||||
|
'newVersionUpdate': 'Оновити зараз',
|
||||||
|
'newVersionLater': 'Пізніше',
|
||||||
|
'extensions': 'Розширення',
|
||||||
|
'extensionInstallUrl': 'URL для встановлення',
|
||||||
|
'extensionInstallSuccess': 'Встановлено успішно',
|
||||||
|
'extensionUpdateSuccess': 'Оновлено успішно',
|
||||||
|
'extensionDelete': 'Видалити розширення',
|
||||||
|
'extensionAlreadyLatest': 'Вже остання версія',
|
||||||
|
'extensionFind': 'Знайти розширення',
|
||||||
|
'extensionDevelop': 'Розробка розширень',
|
||||||
|
'history': 'Історія',
|
||||||
|
'clearHistory': 'Очистити історію',
|
||||||
|
'noHistoryFound': 'Історію не знайдено',
|
||||||
|
'serviceTitle': 'Служба завантаження',
|
||||||
|
'serviceText': 'Працює',
|
||||||
|
'network': 'Мережа',
|
||||||
|
'proxy': 'Проксі',
|
||||||
|
'noProxy': 'Без проксі',
|
||||||
|
'systemProxy': 'Системний проксі',
|
||||||
|
'customProxy': 'Користувацький проксі',
|
||||||
|
'server': 'Сервер',
|
||||||
|
'username': 'Ім’я користувача',
|
||||||
|
'password': 'Пароль',
|
||||||
|
'thanks': 'Подяки',
|
||||||
|
'thanksDesc':
|
||||||
|
'Дякуємо всім учасникам, які допомогли створити та розвивати спільноту Gopeed!',
|
||||||
|
'browserExtension': 'Розширення для браузера',
|
||||||
|
'launchAtStartup': 'Запускати під час завантаження системи',
|
||||||
|
'seedConfig': 'Налаштування роздачі',
|
||||||
|
'seedKeep': 'Роздавати доки не зупиню вручну',
|
||||||
|
'seedRatio': 'Коефіцієнт роздачі',
|
||||||
|
'seedTime': 'Час роздачі (хвилини)',
|
||||||
|
'setAsDefaultBtClient': 'Встановити як клієнт BT за замовчуванням',
|
||||||
|
'taskDetail': 'Деталі завдання',
|
||||||
|
'taskName': 'Назва завдання',
|
||||||
|
'taskUrl': 'URL завдання',
|
||||||
|
'downloadPath': 'Шлях завантаження',
|
||||||
|
'skipVerifyCert': 'Пропустити перевірку сертифіката',
|
||||||
|
'name': 'Назва',
|
||||||
|
'size': 'Розмір',
|
||||||
|
'unknown': 'Невідомо',
|
||||||
|
'fileSelectedCount': 'Файлів: ',
|
||||||
|
'fileSelectedSize': 'Розмір: ',
|
||||||
|
'httpHeaderName': 'Назва заголовка HTTP',
|
||||||
|
'httpHeaderValue': 'Значення заголовка HTTP',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -51,6 +51,7 @@ const zhCN = {
|
|||||||
'themeLight': '明亮主题',
|
'themeLight': '明亮主题',
|
||||||
'themeDark': '暗黑主题',
|
'themeDark': '暗黑主题',
|
||||||
'locale': '语言',
|
'locale': '语言',
|
||||||
|
'notifyWhenNewVersion': '有新版本时提醒',
|
||||||
'about': '关于',
|
'about': '关于',
|
||||||
'homepage': '主页',
|
'homepage': '主页',
|
||||||
'version': '版本',
|
'version': '版本',
|
||||||
@@ -116,5 +117,11 @@ const zhCN = {
|
|||||||
'fileSelectedSize': '大小:',
|
'fileSelectedSize': '大小:',
|
||||||
'httpHeaderName': '请求头名称',
|
'httpHeaderName': '请求头名称',
|
||||||
'httpHeaderValue': '请求头值',
|
'httpHeaderValue': '请求头值',
|
||||||
|
'login': '登录',
|
||||||
|
'username_required': '请输入用户名',
|
||||||
|
'password_required': '请输入密码',
|
||||||
|
'login_success': '登录成功',
|
||||||
|
'login_failed': '登录失败,请检查用户名和密码',
|
||||||
|
'login_failed_network': '登录失败,请检查网络连接',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import 'langs/de_de.dart';
|
||||||
import 'langs/en_us.dart';
|
import 'langs/en_us.dart';
|
||||||
import 'langs/fa_ir.dart';
|
import 'langs/fa_ir.dart';
|
||||||
import 'langs/fr_fr.dart';
|
import 'langs/fr_fr.dart';
|
||||||
@@ -14,6 +15,9 @@ import 'langs/vi_vn.dart';
|
|||||||
import 'langs/zh_cn.dart';
|
import 'langs/zh_cn.dart';
|
||||||
import 'langs/zh_tw.dart';
|
import 'langs/zh_tw.dart';
|
||||||
import 'langs/es_es.dart';
|
import 'langs/es_es.dart';
|
||||||
|
import 'langs/uk_ua.dart';
|
||||||
|
import 'langs/hu_hu.dart';
|
||||||
|
import 'langs/pt_br.dart';
|
||||||
|
|
||||||
final messages = _Messages();
|
final messages = _Messages();
|
||||||
|
|
||||||
@@ -35,5 +39,9 @@ class _Messages extends Translations {
|
|||||||
...idID,
|
...idID,
|
||||||
...frFR,
|
...frFR,
|
||||||
...esES,
|
...esES,
|
||||||
|
...ukUA,
|
||||||
|
...huHU,
|
||||||
|
...ptBR,
|
||||||
|
...deDE
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+49
-19
@@ -1,6 +1,7 @@
|
|||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import 'util/locale_manager.dart';
|
|||||||
import 'util/log_util.dart';
|
import 'util/log_util.dart';
|
||||||
import 'util/package_info.dart';
|
import 'util/package_info.dart';
|
||||||
import 'util/scheme_register/scheme_register.dart';
|
import 'util/scheme_register/scheme_register.dart';
|
||||||
|
import 'util/updater.dart';
|
||||||
import 'util/util.dart';
|
import 'util/util.dart';
|
||||||
|
|
||||||
class Args {
|
class Args {
|
||||||
@@ -22,6 +24,8 @@ class Args {
|
|||||||
|
|
||||||
bool hidden = false;
|
bool hidden = false;
|
||||||
|
|
||||||
|
Args();
|
||||||
|
|
||||||
Args.parse(List<String> args) {
|
Args.parse(List<String> args) {
|
||||||
final parser = ArgParser();
|
final parser = ArgParser();
|
||||||
parser.addFlag(flagHidden);
|
parser.addFlag(flagHidden);
|
||||||
@@ -31,7 +35,22 @@ class Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main(List<String> arguments) async {
|
void main(List<String> arguments) async {
|
||||||
final args = Args.parse(arguments);
|
Args args;
|
||||||
|
// Parse url scheme arguments, e.g. gopeed:?hidden=true
|
||||||
|
// TODO: macos open url handle
|
||||||
|
// TODO: macos updater test
|
||||||
|
if (arguments.firstOrNull?.startsWith("gopeed:") == true) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(arguments.first);
|
||||||
|
args = Args()..hidden = uri.queryParameters["hidden"] == "true";
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
args = Args.parse([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = Args.parse(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
await init(args);
|
await init(args);
|
||||||
onStart();
|
onStart();
|
||||||
|
|
||||||
@@ -40,6 +59,9 @@ void main(List<String> arguments) async {
|
|||||||
|
|
||||||
Future<void> init(Args args) async {
|
Future<void> init(Args args) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
if (Util.isMobile()) {
|
||||||
|
FlutterForegroundTask.initCommunicationPort();
|
||||||
|
}
|
||||||
await Util.initStorageDir();
|
await Util.initStorageDir();
|
||||||
await Database.instance.init();
|
await Database.instance.init();
|
||||||
if (Util.isDesktop()) {
|
if (Util.isDesktop()) {
|
||||||
@@ -89,26 +111,34 @@ Future<void> init(Args args) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
try {
|
if (Util.isDesktop()) {
|
||||||
registerUrlScheme("gopeed");
|
|
||||||
if (controller.downloaderConfig.value.extra.defaultBtClient) {
|
|
||||||
registerDefaultTorrentClient();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.e("register scheme fail", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await installHost();
|
|
||||||
} catch (e) {
|
|
||||||
logger.e("browser extension host binary install fail", e);
|
|
||||||
}
|
|
||||||
for (final browser in Browser.values) {
|
|
||||||
try {
|
try {
|
||||||
await installManifest(browser);
|
registerUrlScheme("gopeed");
|
||||||
|
if (controller.downloaderConfig.value.extra.defaultBtClient) {
|
||||||
|
registerDefaultTorrentClient();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.e(
|
logger.e("register scheme fail", e);
|
||||||
"browser [${browser.name}] extension host integration fail", e);
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await installHost();
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("browser extension host binary install fail", e);
|
||||||
|
}
|
||||||
|
for (final browser in Browser.values) {
|
||||||
|
try {
|
||||||
|
await installManifest(browser);
|
||||||
|
} catch (e) {
|
||||||
|
logger.e(
|
||||||
|
"browser [${browser.name}] extension host integration fail", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await installUpdater();
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("updater install fail", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'arch_stub.dart'
|
||||||
|
if (dart.library.io) 'entry/arch_native.dart'
|
||||||
|
if (dart.library.html) 'entry/arch_web.dart';
|
||||||
|
|
||||||
|
// Copy from pkg/sky_engine/lib/ffi/abi.dart
|
||||||
|
enum Architecture {
|
||||||
|
arm,
|
||||||
|
arm64,
|
||||||
|
ia32,
|
||||||
|
x64,
|
||||||
|
riscv32,
|
||||||
|
riscv64,
|
||||||
|
}
|
||||||
|
|
||||||
|
Architecture getArch() => doGetArch();
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import 'arch.dart';
|
||||||
|
|
||||||
|
Architecture doGetArch() => throw UnimplementedError();
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// ignore: avoid_web_libraries_in_flutter
|
||||||
|
import 'dart:ffi';
|
||||||
|
|
||||||
|
import '../arch.dart';
|
||||||
|
|
||||||
|
Architecture doGetArch() {
|
||||||
|
final currentAbi = Abi.current().toString();
|
||||||
|
final archName = currentAbi.split("_")[1];
|
||||||
|
final arch = Architecture.values.firstWhere(
|
||||||
|
(element) => element.name == archName,
|
||||||
|
orElse: () => Architecture.x64);
|
||||||
|
return arch;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import '../arch.dart';
|
||||||
|
|
||||||
|
Architecture doGetArch() {
|
||||||
|
return Architecture.x64;
|
||||||
|
}
|
||||||
+27
-52
@@ -1,11 +1,10 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:win32_registry/win32_registry.dart';
|
import 'package:win32_registry/win32_registry.dart';
|
||||||
|
|
||||||
|
import '../../util.dart';
|
||||||
import '../../win32.dart';
|
import '../../win32.dart';
|
||||||
import '../browser_extension_host.dart';
|
import '../browser_extension_host.dart';
|
||||||
|
|
||||||
@@ -27,33 +26,9 @@ const _firefoxNativeHostsKey = r'Software\Mozilla\NativeMessagingHosts';
|
|||||||
|
|
||||||
/// Install host binary for browser extension
|
/// Install host binary for browser extension
|
||||||
Future<void> doInstallHost() async {
|
Future<void> doInstallHost() async {
|
||||||
final hostPath =
|
final hostPath = await Util.homePathJoin(_hostExecName);
|
||||||
path.join((await getApplicationSupportDirectory()).path, _hostExecName);
|
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
|
||||||
|
executable: true);
|
||||||
Future<List<int>> getHostAssetData() async {
|
|
||||||
final hostAsset = await rootBundle.load('assets/host/$_hostExecName');
|
|
||||||
return hostAsset.buffer
|
|
||||||
.asUint8List(hostAsset.offsetInBytes, hostAsset.lengthInBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if host binary is not installed
|
|
||||||
if (!await File(hostPath).exists()) {
|
|
||||||
final hostData = await getHostAssetData();
|
|
||||||
final file = File(hostPath);
|
|
||||||
await file.writeAsBytes(hostData);
|
|
||||||
// Add execute permission
|
|
||||||
if (!Platform.isWindows) {
|
|
||||||
await Process.run('chmod', ['+x', hostPath]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check if host binary is outdated
|
|
||||||
final hostAssetData = await getHostAssetData();
|
|
||||||
final hostFileData = await File(hostPath).readAsBytes();
|
|
||||||
if (hostAssetData.length != hostFileData.length) {
|
|
||||||
await File(hostPath).writeAsBytes(hostAssetData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if specified browser is installed
|
/// Check if specified browser is installed
|
||||||
@@ -87,7 +62,7 @@ Future<bool> doCheckBrowserInstalled(Browser browser) async {
|
|||||||
Future<bool> doCheckManifestInstalled(Browser browser) async {
|
Future<bool> doCheckManifestInstalled(Browser browser) async {
|
||||||
if (await checkBrowserInstalled(browser) == false) return false;
|
if (await checkBrowserInstalled(browser) == false) return false;
|
||||||
|
|
||||||
final manifestPath = _getManifestPath(browser);
|
final manifestPath = await _getManifestPath(browser);
|
||||||
if (manifestPath == null) return false;
|
if (manifestPath == null) return false;
|
||||||
|
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
@@ -102,7 +77,7 @@ Future<bool> doCheckManifestInstalled(Browser browser) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final existingContent = await File(manifestPath).readAsString();
|
final existingContent = await File(manifestPath).readAsString();
|
||||||
final expectedContent = await _getManifestContent();
|
final expectedContent = await _getManifestContent(browser);
|
||||||
return existingContent == expectedContent;
|
return existingContent == expectedContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +86,8 @@ Future<void> doInstallManifest(Browser browser) async {
|
|||||||
if (await checkBrowserInstalled(browser) == false) return;
|
if (await checkBrowserInstalled(browser) == false) return;
|
||||||
if (await checkManifestInstalled(browser)) return;
|
if (await checkManifestInstalled(browser)) return;
|
||||||
|
|
||||||
final manifestPath = _getManifestPath(browser)!;
|
final manifestPath = (await _getManifestPath(browser))!;
|
||||||
final manifestContent = await _getManifestContent();
|
final manifestContent = await _getManifestContent(browser);
|
||||||
final manifestDir = path.dirname(manifestPath);
|
final manifestDir = path.dirname(manifestPath);
|
||||||
await Directory(manifestDir).create(recursive: true);
|
await Directory(manifestDir).create(recursive: true);
|
||||||
await File(manifestPath).writeAsString(manifestContent);
|
await File(manifestPath).writeAsString(manifestContent);
|
||||||
@@ -234,11 +209,11 @@ List<String> _getUnixExecutablePaths(Browser browser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _getManifestPath(Browser browser) {
|
Future<String?> _getManifestPath(Browser browser) async {
|
||||||
|
final manifestName =
|
||||||
|
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
final execPath = Platform.resolvedExecutable;
|
return await Util.homePathJoin(manifestName);
|
||||||
final execDir = path.dirname(execPath);
|
|
||||||
return path.join(execDir, '$_hostName.json');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final home =
|
final home =
|
||||||
@@ -249,25 +224,25 @@ String? _getManifestPath(Browser browser) {
|
|||||||
switch (browser) {
|
switch (browser) {
|
||||||
case Browser.chrome:
|
case Browser.chrome:
|
||||||
return path.join(home, 'Library', 'Application Support', 'Google',
|
return path.join(home, 'Library', 'Application Support', 'Google',
|
||||||
'Chrome', 'NativeMessagingHosts', '$_hostName.json');
|
'Chrome', 'NativeMessagingHosts', manifestName);
|
||||||
case Browser.edge:
|
case Browser.edge:
|
||||||
return path.join(home, 'Library', 'Application Support',
|
return path.join(home, 'Library', 'Application Support',
|
||||||
'Microsoft Edge', 'NativeMessagingHosts', '$_hostName.json');
|
'Microsoft Edge', 'NativeMessagingHosts', manifestName);
|
||||||
case Browser.firefox:
|
case Browser.firefox:
|
||||||
return path.join(home, 'Library', 'Application Support', 'Mozilla',
|
return path.join(home, 'Library', 'Application Support', 'Mozilla',
|
||||||
'NativeMessagingHosts', '$_hostName.json');
|
'NativeMessagingHosts', manifestName);
|
||||||
}
|
}
|
||||||
} else if (Platform.isLinux) {
|
} else if (Platform.isLinux) {
|
||||||
switch (browser) {
|
switch (browser) {
|
||||||
case Browser.chrome:
|
case Browser.chrome:
|
||||||
return path.join(home, '.config', 'google-chrome',
|
return path.join(home, '.config', 'google-chrome',
|
||||||
'NativeMessagingHosts', '$_hostName.json');
|
'NativeMessagingHosts', manifestName);
|
||||||
case Browser.edge:
|
case Browser.edge:
|
||||||
return path.join(home, '.config', 'microsoft-edge',
|
return path.join(home, '.config', 'microsoft-edge',
|
||||||
'NativeMessagingHosts', '$_hostName.json');
|
'NativeMessagingHosts', manifestName);
|
||||||
case Browser.firefox:
|
case Browser.firefox:
|
||||||
return path.join(
|
return path.join(
|
||||||
home, '.mozilla', 'native-messaging-hosts', '$_hostName.json');
|
home, '.mozilla', 'native-messaging-hosts', manifestName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -283,20 +258,20 @@ Future<bool> _checkWindowsRegistry(String keyPath) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getManifestContent() async {
|
Future<String> _getManifestContent(Browser browser) async {
|
||||||
final hostPath =
|
final hostPath = await Util.homePathJoin(_hostExecName);
|
||||||
path.join((await getApplicationSupportDirectory()).path, _hostExecName);
|
|
||||||
final manifest = {
|
final manifest = {
|
||||||
'name': _hostName,
|
'name': _hostName,
|
||||||
'description': 'Gopeed browser extension host',
|
'description': 'Gopeed browser extension host',
|
||||||
'path': hostPath,
|
'path': hostPath,
|
||||||
'type': 'stdio',
|
'type': 'stdio',
|
||||||
'allowed_origins': [
|
if (browser != Browser.firefox)
|
||||||
'chrome-extension://$_chromeExtensionId/',
|
'allowed_origins': [
|
||||||
'chrome-extension://$_edgeExtensionId/',
|
'chrome-extension://$_chromeExtensionId/',
|
||||||
..._debugExtensionIds.map((id) => 'chrome-extension://$id/'),
|
'chrome-extension://$_edgeExtensionId/',
|
||||||
],
|
..._debugExtensionIds.map((id) => 'chrome-extension://$id/'),
|
||||||
'allowed_extensions': [_firefoxExtensionId],
|
],
|
||||||
|
if (browser == Browser.firefox) 'allowed_extensions': [_firefoxExtensionId],
|
||||||
};
|
};
|
||||||
return const JsonEncoder.withIndent(' ').convert(manifest);
|
return const JsonEncoder.withIndent(' ').convert(manifest);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
// List of mirrors for GitHub
|
||||||
|
const _sourceMirror = [
|
||||||
|
// github.tbedu.top: https://github.tbedu.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
|
["https://github.tbedu.top/", r"(.*)"],
|
||||||
|
// fastgit.cc: https://fastgit.cc/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
|
["https://fastgit.cc/", r"(.*)"],
|
||||||
|
// gitproxy.click: https://gitproxy.click/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
|
["https://gitproxy.click/", r"(.*)"],
|
||||||
|
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||||
|
[
|
||||||
|
"https://fastly.jsdelivr.net/gh",
|
||||||
|
r".*raw.githubusercontent.com(/.*)(/.*)/(?:master|main)(/.*)"
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// List of mirrors for GitHub release
|
||||||
|
const _assertMirror = [
|
||||||
|
// github.tbedu.top: https://https://github.tbedu.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||||
|
["https://github.tbedu.top/", r"(.*)"],
|
||||||
|
// fastgit.cc: https://fastgit.cc/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||||
|
["https://fastgit.cc/", r"(.*)"],
|
||||||
|
// gitproxy.click: https://gitproxy.click/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||||
|
["https://gitproxy.click/", r"(.*)"],
|
||||||
|
];
|
||||||
|
|
||||||
|
enum MirrorType {
|
||||||
|
githubSource,
|
||||||
|
githubRelease,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auto detect the best mirror for the given [rawUrl] and [type]
|
||||||
|
Future<String> githubAutoMirror(String rawUrl, MirrorType type) async {
|
||||||
|
final mirrorUrls = githubMirrorUrls(rawUrl, type);
|
||||||
|
|
||||||
|
// Ping all mirrors and get the fastest one
|
||||||
|
final pingResult = await Future.wait(mirrorUrls.map((e) async {
|
||||||
|
final client = Dio()
|
||||||
|
..options.sendTimeout = const Duration(seconds: 3)
|
||||||
|
..options.connectTimeout = const Duration(seconds: 3);
|
||||||
|
var time = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
try {
|
||||||
|
final response = await client.head(e);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
time = DateTime.now().millisecondsSinceEpoch - time;
|
||||||
|
return (e, time);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
return (e, -1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
var list = pingResult.where((e) => e.$2 != -1).toList()
|
||||||
|
..sort((a, b) => a.$2.compareTo(b.$2));
|
||||||
|
if (list.isNotEmpty) {
|
||||||
|
return list.first.$1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> githubMirrorUrls(String rawUrl, MirrorType type) {
|
||||||
|
final mirrors = switch (type) {
|
||||||
|
MirrorType.githubSource => _sourceMirror,
|
||||||
|
MirrorType.githubRelease => _assertMirror,
|
||||||
|
};
|
||||||
|
|
||||||
|
final ret = <String>[];
|
||||||
|
for (final cdn in mirrors) {
|
||||||
|
final reg = RegExp(cdn[1]);
|
||||||
|
final match = reg.firstMatch(rawUrl.toString());
|
||||||
|
var matchStr = "";
|
||||||
|
for (var i = 1; i <= match!.groupCount; i++) {
|
||||||
|
matchStr += match.group(i)!;
|
||||||
|
}
|
||||||
|
ret.add("${cdn[0]}$matchStr");
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:install_plugin/install_plugin.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../api/api.dart';
|
||||||
|
import '../app/views/outlined_button_loading.dart';
|
||||||
|
import 'arch/arch.dart';
|
||||||
|
import 'github_mirror.dart';
|
||||||
|
import 'log_util.dart';
|
||||||
|
import 'message.dart';
|
||||||
|
import 'package_info.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
enum Channel {
|
||||||
|
windowsInstaller,
|
||||||
|
windowsPortable,
|
||||||
|
macosDmg,
|
||||||
|
linuxFlathub,
|
||||||
|
linuxSnap,
|
||||||
|
linuxDeb,
|
||||||
|
linuxAppImage,
|
||||||
|
androidApk,
|
||||||
|
iosIpa,
|
||||||
|
docker,
|
||||||
|
}
|
||||||
|
|
||||||
|
const _channelEnv = String.fromEnvironment("UPDATE_CHANNEL");
|
||||||
|
final _channel =
|
||||||
|
Channel.values.where((e) => e.name == _channelEnv).firstOrNull ??
|
||||||
|
() {
|
||||||
|
if (Util.isWindows()) {
|
||||||
|
return Channel.windowsPortable;
|
||||||
|
} else if (Util.isMacos()) {
|
||||||
|
return Channel.macosDmg;
|
||||||
|
} else if (Util.isAndroid()) {
|
||||||
|
return Channel.androidApk;
|
||||||
|
} else if (Util.isIOS()) {
|
||||||
|
return Channel.iosIpa;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
final _updaterBin = "updater${Util.isWindows() ? ".exe" : ""}";
|
||||||
|
|
||||||
|
Future<void> installUpdater() async {
|
||||||
|
await Util.installAsset(
|
||||||
|
'assets/exec/$_updaterBin', await Util.homePathJoin(_updaterBin),
|
||||||
|
executable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
class VersionInfo {
|
||||||
|
final String version;
|
||||||
|
final String changeLog;
|
||||||
|
|
||||||
|
VersionInfo(this.version, this.changeLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<VersionInfo?> checkUpdate() async {
|
||||||
|
String? releaseDataStr;
|
||||||
|
try {
|
||||||
|
releaseDataStr = (await proxyRequest(
|
||||||
|
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
||||||
|
.data;
|
||||||
|
} catch (e) {
|
||||||
|
releaseDataStr =
|
||||||
|
(await proxyRequest("https://gopeed.com/api/release")).data;
|
||||||
|
}
|
||||||
|
if (releaseDataStr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final releaseData = jsonDecode(releaseDataStr);
|
||||||
|
final tagName = releaseData["tag_name"];
|
||||||
|
if (tagName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final latestVersion = releaseData["tag_name"].substring(1);
|
||||||
|
|
||||||
|
// compare version x.y.z to x.y.z
|
||||||
|
final currentVersion = packageInfo.version;
|
||||||
|
var isNewVersion = false;
|
||||||
|
if (latestVersion != currentVersion) {
|
||||||
|
final currentVersionList = currentVersion.split(".");
|
||||||
|
final latestVersionList = latestVersion.split(".");
|
||||||
|
for (var i = 0; i < currentVersionList.length; i++) {
|
||||||
|
if (int.parse(latestVersionList[i]) > int.parse(currentVersionList[i])) {
|
||||||
|
isNewVersion = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNewVersion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return VersionInfo(latestVersion, releaseData["body"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showUpdateDialog(
|
||||||
|
BuildContext context, VersionInfo versionInfo) async {
|
||||||
|
final fullChangeLog = versionInfo.changeLog;
|
||||||
|
final isZh = Get.locale?.languageCode == "zh";
|
||||||
|
final changeLogRegex = isZh
|
||||||
|
? RegExp(r"(#\s+更新日志.*)", multiLine: true, dotAll: true)
|
||||||
|
: RegExp(r"(# Release notes.*)#\s+更新日志", multiLine: true, dotAll: true);
|
||||||
|
final changeLog = changeLogRegex.firstMatch(fullChangeLog)?.group(1) ?? "";
|
||||||
|
await showDialog(
|
||||||
|
context: Get.context!,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) {
|
||||||
|
bool isDownloading = false;
|
||||||
|
double progress = 0;
|
||||||
|
int total = 0;
|
||||||
|
final buttonController = OutlinedButtonLoadingController();
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
final dialogWidth =
|
||||||
|
screenSize.width < 500 ? screenSize.width * 0.9 : 500.0;
|
||||||
|
final dialogHeight =
|
||||||
|
screenSize.height < 600 ? screenSize.height * 0.8 : 400.0;
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: dialogWidth,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'newVersionTitle'
|
||||||
|
.trParams({'version': versionInfo.version}),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
height: dialogHeight * 0.5,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border:
|
||||||
|
Border.all(color: Theme.of(context).dividerColor),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context).copyWith(
|
||||||
|
scrollbars: true,
|
||||||
|
),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final controller = ScrollController();
|
||||||
|
return Scrollbar(
|
||||||
|
controller: controller,
|
||||||
|
thumbVisibility: true,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: controller,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children:
|
||||||
|
_parseMarkdown(changeLog, context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isDownloading) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${(progress * 100).toStringAsFixed(1)}%',
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
total == 0
|
||||||
|
? ''
|
||||||
|
: '${Util.fmtByte((total * progress).toInt())} / ${Util.fmtByte(total)}',
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: isDownloading ? null : () => Get.back(),
|
||||||
|
child: Text(
|
||||||
|
'newVersionLater'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isDownloading
|
||||||
|
? Theme.of(context).disabledColor
|
||||||
|
: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButtonLoading(
|
||||||
|
controller: buttonController,
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
isDownloading = true;
|
||||||
|
});
|
||||||
|
buttonController.start();
|
||||||
|
try {
|
||||||
|
await _update(versionInfo.version,
|
||||||
|
(received, fileTotal) {
|
||||||
|
setState(() {
|
||||||
|
total = fileTotal;
|
||||||
|
progress = received / fileTotal;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
showErrorMessage(e);
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
isDownloading = false;
|
||||||
|
});
|
||||||
|
buttonController.stop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('newVersionUpdate'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _parseMarkdown(String markdown, BuildContext context) {
|
||||||
|
final List<Widget> widgets = [];
|
||||||
|
final lines = markdown.split('\n');
|
||||||
|
|
||||||
|
for (final line in lines) {
|
||||||
|
if (line.trim().isEmpty) continue;
|
||||||
|
if (line.startsWith('# ')) {
|
||||||
|
// H1 header
|
||||||
|
widgets.add(Text(
|
||||||
|
line.substring(2),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
} else if (line.startsWith('## ')) {
|
||||||
|
// H2 header
|
||||||
|
widgets.add(Text(
|
||||||
|
line.substring(3),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
} else if (line.trim().startsWith('- ')) {
|
||||||
|
// List item
|
||||||
|
widgets.add(Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('• ', style: TextStyle(fontSize: 14)),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
line.substring(line.indexOf('-') + 1).trim().replaceFirst(
|
||||||
|
RegExp(r'@[^\s]*\s\(#\d+\)'),
|
||||||
|
''), // Remove contributor and pr number
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Normal text
|
||||||
|
widgets.add(Text(
|
||||||
|
line,
|
||||||
|
style: const TextStyle(fontSize: 14),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add spacing between elements
|
||||||
|
widgets.add(const SizedBox(height: 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _update(String version, Function(int, int) onProgress) async {
|
||||||
|
var newVersionAssetPath = "";
|
||||||
|
final newVersionAssetName = _getAssetName(version);
|
||||||
|
|
||||||
|
// Need to download the asset
|
||||||
|
if (newVersionAssetName.isNotEmpty) {
|
||||||
|
final downloadUrl =
|
||||||
|
'https://github.com/GopeedLab/gopeed/releases/download/v$version/$newVersionAssetName';
|
||||||
|
newVersionAssetPath = await _getAssetPath(version);
|
||||||
|
|
||||||
|
if (downloadUrl.isNotEmpty) {
|
||||||
|
final fastDownloadUrl =
|
||||||
|
await githubAutoMirror(downloadUrl, MirrorType.githubRelease);
|
||||||
|
final downloadClient = Dio();
|
||||||
|
await downloadClient.download(fastDownloadUrl, newVersionAssetPath,
|
||||||
|
onReceiveProgress: onProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_channel) {
|
||||||
|
case Channel.windowsInstaller:
|
||||||
|
case Channel.windowsPortable:
|
||||||
|
case Channel.macosDmg:
|
||||||
|
case Channel.linuxFlathub:
|
||||||
|
case Channel.linuxSnap:
|
||||||
|
case Channel.linuxDeb:
|
||||||
|
final updaterPath = await Util.homePathJoin(_updaterBin);
|
||||||
|
// Check the updater binary is exists
|
||||||
|
if (!await File(updaterPath).exists()) {
|
||||||
|
await launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*Usage of updater command:
|
||||||
|
-pid int
|
||||||
|
PID of the process to update
|
||||||
|
-channel string
|
||||||
|
Update channel
|
||||||
|
-asset string
|
||||||
|
Path to the package asset
|
||||||
|
-exeDir string
|
||||||
|
Directory of the entry executable
|
||||||
|
-log string
|
||||||
|
Log file path
|
||||||
|
*/
|
||||||
|
await Process.run(updaterPath, [
|
||||||
|
"-pid",
|
||||||
|
pid.toString(),
|
||||||
|
"-channel",
|
||||||
|
_channel!.name,
|
||||||
|
"-asset",
|
||||||
|
newVersionAssetPath,
|
||||||
|
"-exeDir",
|
||||||
|
path.dirname(Platform.resolvedExecutable),
|
||||||
|
"-log",
|
||||||
|
path.join(logsDir(), "updater.log")
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
case Channel.androidApk:
|
||||||
|
await InstallPlugin.installApk(newVersionAssetPath);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
await launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getAssetName(String version) {
|
||||||
|
final arch = getArch();
|
||||||
|
|
||||||
|
String commonArchName() {
|
||||||
|
return switch (arch) {
|
||||||
|
Architecture.ia32 => "386",
|
||||||
|
Architecture.x64 => "amd64",
|
||||||
|
_ => arch.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_channel) {
|
||||||
|
case Channel.windowsInstaller:
|
||||||
|
return 'Gopeed-v$version-windows-${commonArchName()}.zip';
|
||||||
|
case Channel.windowsPortable:
|
||||||
|
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
|
||||||
|
case Channel.macosDmg:
|
||||||
|
return 'Gopeed-v$version-macos-${commonArchName()}.dmg';
|
||||||
|
case Channel.linuxDeb:
|
||||||
|
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
|
||||||
|
case Channel.androidApk:
|
||||||
|
final apkArchName = switch (arch) {
|
||||||
|
Architecture.arm => "armeabi-v7a",
|
||||||
|
Architecture.arm64 => "arm64-v8a",
|
||||||
|
Architecture.x64 => "x86_64",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
var apkNamePrefix = "Gopeed-v$version-android";
|
||||||
|
if (apkArchName != null) {
|
||||||
|
apkNamePrefix += "-$apkArchName";
|
||||||
|
}
|
||||||
|
return "$apkNamePrefix.apk";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getAssetPath(String version) async {
|
||||||
|
return path.join(
|
||||||
|
(await getTemporaryDirectory()).path, _getAssetName(version));
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
@@ -52,7 +54,8 @@ class Util {
|
|||||||
static Future<void> initStorageDir() async {
|
static Future<void> initStorageDir() async {
|
||||||
var storageDir = "";
|
var storageDir = "";
|
||||||
if (Util.isWindows()) {
|
if (Util.isWindows()) {
|
||||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
storageDir =
|
||||||
|
path.join(File(Platform.resolvedExecutable).parent.path, "storage");
|
||||||
} else if (!Util.isWeb()) {
|
} else if (!Util.isWeb()) {
|
||||||
if (Util.isLinux()) {
|
if (Util.isLinux()) {
|
||||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
storageDir = File(Platform.resolvedExecutable).parent.path;
|
||||||
@@ -153,4 +156,50 @@ class Util {
|
|||||||
timer = Timer(Duration(milliseconds: ms), fn);
|
timer = Timer(Duration(milliseconds: ms), fn);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<String> homePathJoin(String fileName) async {
|
||||||
|
if (Util.isWindows()) {
|
||||||
|
final execPath = Platform.resolvedExecutable;
|
||||||
|
final execDir = path.dirname(execPath);
|
||||||
|
return path.join(execDir, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final dir = await getApplicationSupportDirectory();
|
||||||
|
return path.join(dir.path, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> installAsset(String assetPath, String targetPath,
|
||||||
|
{bool executable = false}) async {
|
||||||
|
Future<List<int>> getAssetData() async {
|
||||||
|
final asset = await rootBundle.load(assetPath);
|
||||||
|
return asset.buffer.asUint8List(asset.offsetInBytes, asset.lengthInBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target file is not installed
|
||||||
|
if (!await File(targetPath).exists()) {
|
||||||
|
final assetData = await getAssetData();
|
||||||
|
final file = File(targetPath);
|
||||||
|
await file.writeAsBytes(assetData);
|
||||||
|
// Add execute permission when file first created
|
||||||
|
if (executable && !Platform.isWindows) {
|
||||||
|
await Process.run('chmod', ['+x', targetPath]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target file needs to be updated
|
||||||
|
final assetData = await getAssetData();
|
||||||
|
if (_md5(assetData) != await _md5File(File(targetPath))) {
|
||||||
|
await File(targetPath).writeAsBytes(assetData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _md5(List<int> data) {
|
||||||
|
return md5.convert(data).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> _md5File(File file) async {
|
||||||
|
return file.openRead().transform(md5).first.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ Terminal=false
|
|||||||
Exec=gopeed %U
|
Exec=gopeed %U
|
||||||
Icon=com.gopeed.Gopeed
|
Icon=com.gopeed.Gopeed
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Utility;Internet
|
Categories=Utility;Network;
|
||||||
Keywords=Bittorrent;Downloader;
|
Keywords=Bittorrent;Downloader;
|
||||||
MimeType=x-scheme-handler/gopeed;x-scheme-handler/magnet;application/x-bittorrent;
|
MimeType=x-scheme-handler/gopeed;x-scheme-handler/magnet;application/x-bittorrent;
|
||||||
+106
-8
@@ -271,7 +271,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.4+2"
|
version: "0.3.4+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
@@ -286,6 +286,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dart_ipc:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dart_ipc
|
||||||
|
sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -448,6 +456,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.23"
|
version: "2.0.23"
|
||||||
|
flutter_svg:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_svg
|
||||||
|
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.17"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -546,6 +562,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.3.0"
|
||||||
|
install_plugin:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||||
|
resolved-ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||||
|
url: "https://github.com/hui-z/flutter_install_plugin.git"
|
||||||
|
source: git
|
||||||
|
version: "2.1.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -714,6 +739,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:
|
||||||
@@ -754,6 +819,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1187,11 +1260,12 @@ packages:
|
|||||||
tray_manager:
|
tray_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: tray_manager
|
path: "packages/tray_manager"
|
||||||
sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7
|
ref: main
|
||||||
url: "https://pub.dev"
|
resolved-ref: "91cdbdc12517d0d832aca715542b9d0329739d5d"
|
||||||
source: hosted
|
url: "https://github.com/monkeyWie/tray_manager.git"
|
||||||
version: "0.2.4"
|
source: git
|
||||||
|
version: "0.5.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1280,6 +1354,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.1"
|
version: "4.5.1"
|
||||||
|
vector_graphics:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics
|
||||||
|
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.18"
|
||||||
|
vector_graphics_codec:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_codec
|
||||||
|
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.13"
|
||||||
|
vector_graphics_compiler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_compiler
|
||||||
|
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.16"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1324,10 +1422,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.3"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
+16
-3
@@ -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.7+1
|
version: 1.8.2+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
@@ -58,7 +58,11 @@ dependencies:
|
|||||||
form_builder_validators: ^11.0.0
|
form_builder_validators: ^11.0.0
|
||||||
flutter_foreground_task: ^8.2.0
|
flutter_foreground_task: ^8.2.0
|
||||||
open_filex: ^4.5.0
|
open_filex: ^4.5.0
|
||||||
tray_manager: ^0.2.4
|
tray_manager:
|
||||||
|
git:
|
||||||
|
url: https://github.com/monkeyWie/tray_manager.git
|
||||||
|
ref: main
|
||||||
|
path: packages/tray_manager
|
||||||
lecle_downloads_path_provider: ^0.0.2+8
|
lecle_downloads_path_provider: ^0.0.2+8
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
launch_at_startup: ^0.3.1
|
launch_at_startup: ^0.3.1
|
||||||
@@ -71,6 +75,14 @@ dependencies:
|
|||||||
contentsize_tabbarview: ^0.0.2
|
contentsize_tabbarview: ^0.0.2
|
||||||
win32_registry: ^1.1.5
|
win32_registry: ^1.1.5
|
||||||
share_handler: ^0.0.22
|
share_handler: ^0.0.22
|
||||||
|
crypto: ^3.0.6
|
||||||
|
open_dir: ^0.0.2+1
|
||||||
|
install_plugin:
|
||||||
|
git:
|
||||||
|
url: https://github.com/hui-z/flutter_install_plugin.git
|
||||||
|
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||||
|
flutter_svg: ^2.0.17
|
||||||
|
dart_ipc: ^1.0.1
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
git:
|
git:
|
||||||
@@ -107,8 +119,9 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/tray_icon/
|
- assets/tray_icon/
|
||||||
- assets/extension/
|
- assets/extension/
|
||||||
|
- assets/icon/
|
||||||
# Browser extension native host binary, see https://github.com/GopeedLab/gopeed/tree/main/cmd/host
|
# Browser extension native host binary, see https://github.com/GopeedLab/gopeed/tree/main/cmd/host
|
||||||
- assets/host/
|
- assets/exec/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|||||||
@@ -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