mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Compare commits
96
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5303e44ead | ||
|
|
8835c2855a | ||
|
|
2ab5a4bb53 | ||
|
|
bfad91dcab | ||
|
|
61a959adda | ||
|
|
f4b839fd11 | ||
|
|
969aeb5e0a | ||
|
|
68d42eb397 | ||
|
|
08603f9510 | ||
|
|
b450bf67bd | ||
|
|
03e26dbea8 | ||
|
|
046a3ac9f9 | ||
|
|
9c5aebaf36 | ||
|
|
a094715d69 | ||
|
|
69bb875172 | ||
|
|
589f10c7ef | ||
|
|
654e57e946 | ||
|
|
b94b99ae09 | ||
|
|
fba990a056 | ||
|
|
cb07ce58bc | ||
|
|
50beef7b08 | ||
|
|
bd39e4110f | ||
|
|
86efcb98c5 | ||
|
|
f3d9873395 | ||
|
|
ef228c1722 | ||
|
|
6863317e9d | ||
|
|
2b83a735cb | ||
|
|
447f599783 | ||
|
|
f986f6253d | ||
|
|
2e1b621250 | ||
|
|
d50947b2e6 | ||
|
|
520a72415c | ||
|
|
a372b67edb | ||
|
|
2d86b5c132 | ||
|
|
63b3144633 | ||
|
|
1b0178fed9 | ||
|
|
83febe0a00 | ||
|
|
987580d400 | ||
|
|
0919936050 | ||
|
|
acc7ed8f6b | ||
|
|
ccb3267fba | ||
|
|
1249493a8a | ||
|
|
107141bbe7 | ||
|
|
b329fee107 | ||
|
|
3ce95f21fd | ||
|
|
f54ed5df5a | ||
|
|
727b7edb84 | ||
|
|
f5891ee03d | ||
|
|
724276ac69 | ||
|
|
a3f559c8e5 | ||
|
|
edd189a8a8 | ||
|
|
c970378fd7 | ||
|
|
8b61343aef | ||
|
|
2cb93a03e2 | ||
|
|
81dfc073a6 | ||
|
|
427921ccab | ||
|
|
f4876c8a0e | ||
|
|
35a3625b11 | ||
|
|
dc9b501e35 | ||
|
|
85fe92a378 | ||
|
|
983d9ad09e | ||
|
|
d9f757aaf0 | ||
|
|
cd409fcfb9 | ||
|
|
366244fa5d | ||
|
|
7a2e9b4fd1 | ||
|
|
203385d3f8 | ||
|
|
aaf73dec24 | ||
|
|
2523282a89 | ||
|
|
3384f5197a | ||
|
|
cd77665f02 | ||
|
|
a22b431150 | ||
|
|
eb7c9ad7f2 | ||
|
|
ea11fc38f1 | ||
|
|
1be16e9f2c | ||
|
|
1d86d14a20 | ||
|
|
5ad15cc4ac | ||
|
|
49604966fd | ||
|
|
bdb8ed177f | ||
|
|
2dc7048cba | ||
|
|
d3750464aa | ||
|
|
5c9bcd3731 | ||
|
|
bdda550b2c | ||
|
|
3308d966f0 | ||
|
|
18041ed9ec | ||
|
|
56b85e545e | ||
|
|
c5af916952 | ||
|
|
a1981328aa | ||
|
|
51244133e0 | ||
|
|
0c17093b1a | ||
|
|
6538b29b62 | ||
|
|
81e90e6b89 | ||
|
|
4f39296664 | ||
|
|
ece5b91dc2 | ||
|
|
d9f04ae928 | ||
|
|
e3ccecbd9c | ||
|
|
fb1c2cb060 |
No files matched your search
+155
-28
@@ -1,46 +1,173 @@
|
|||||||
name-template: 'v$NEXT_PATCH_VERSION 🌈'
|
name-template: "v$NEXT_PATCH_VERSION 🌈"
|
||||||
tag-template: 'v$NEXT_PATCH_VERSION'
|
tag-template: "v$NEXT_PATCH_VERSION"
|
||||||
categories:
|
categories:
|
||||||
- title: '🆕 Features'
|
- title: "🆕 Features"
|
||||||
labels:
|
labels:
|
||||||
- 'feat'
|
- "feat"
|
||||||
- 'feature'
|
- "feature"
|
||||||
- 'enhancement'
|
- "enhancement"
|
||||||
- title: '🐛 Bug Fixes'
|
- title: "🐛 Bug Fixes"
|
||||||
labels:
|
labels:
|
||||||
- 'fix'
|
- "fix"
|
||||||
- 'bugfix'
|
- "bugfix"
|
||||||
- 'bug'
|
- "bug"
|
||||||
- title: '🔧 Performance Improvements'
|
- title: "🔧 Performance Improvements"
|
||||||
labels:
|
labels:
|
||||||
- 'perf'
|
- "perf"
|
||||||
- title: '🧪 Tests'
|
- title: "🧪 Tests"
|
||||||
label: 'test'
|
label: "test"
|
||||||
- title: '🧰 Maintenance'
|
- title: "🧰 Maintenance"
|
||||||
label: 'chore'
|
label: "chore"
|
||||||
- title: '📖 Document'
|
- title: "📖 Document"
|
||||||
label: 'docs'
|
label: "docs"
|
||||||
- title: '🚀 CI/CD'
|
- title: "🚀 CI/CD"
|
||||||
label: 'ci'
|
label: "ci"
|
||||||
- title: '🌎 Internationalization'
|
- title: "🌎 Internationalization"
|
||||||
label: 'translation'
|
label: "translation"
|
||||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
||||||
version-resolver:
|
version-resolver:
|
||||||
major:
|
major:
|
||||||
labels:
|
labels:
|
||||||
- 'major'
|
- "major"
|
||||||
minor:
|
minor:
|
||||||
labels:
|
labels:
|
||||||
- 'minor'
|
- "minor"
|
||||||
patch:
|
patch:
|
||||||
labels:
|
labels:
|
||||||
- 'patch'
|
- "patch"
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">🪟 Windows</td>
|
||||||
|
<td><code>EXE</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🍎 MacOS</td>
|
||||||
|
<td><code>DMG</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6">🐧 Linux</td>
|
||||||
|
<td><code>Flathub</code></td
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
|
|
||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
+200
-49
@@ -4,17 +4,17 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
platform:
|
platform:
|
||||||
description: 'Build platform'
|
description: "Build platform"
|
||||||
required: true
|
required: true
|
||||||
default: 'all'
|
default: "all"
|
||||||
test:
|
test:
|
||||||
description: 'Test mode'
|
description: "Test mode"
|
||||||
required: true
|
required: true
|
||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.22"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -35,7 +35,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-latest
|
||||||
needs: [ get-release ]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -51,10 +51,12 @@ jobs:
|
|||||||
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
|
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
|
||||||
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=$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/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\"
|
||||||
@@ -68,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.
|
||||||
@@ -97,11 +100,13 @@ jobs:
|
|||||||
PrivilegesRequired=lowest
|
PrivilegesRequired=lowest
|
||||||
OutputBaseFilename=gopeed
|
OutputBaseFilename=gopeed
|
||||||
SetupIconFile=..\..\assets\icon\icon.ico
|
SetupIconFile=..\..\assets\icon\icon.ico
|
||||||
|
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
LanguageDetectionMethod=uilanguage
|
LanguageDetectionMethod=uilanguage
|
||||||
ShowLanguageDialog=yes
|
ShowLanguageDialog=yes
|
||||||
|
CloseApplications=force
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
@@ -111,7 +116,7 @@ jobs:
|
|||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs;
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
[UninstallDelete]
|
[UninstallDelete]
|
||||||
@@ -137,7 +142,7 @@ jobs:
|
|||||||
build-macos-arm64-lib:
|
build-macos-arm64-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-latest
|
||||||
needs: [ get-release ]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -148,14 +153,19 @@ 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 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
|
||||||
- uses: actions/upload-artifact@v3
|
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-arm64-lib
|
||||||
path: libgopeed.dylib
|
path: |
|
||||||
|
libgopeed.dylib
|
||||||
|
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-13
|
||||||
needs: [ get-release, build-macos-arm64-lib ]
|
needs: [get-release, build-macos-arm64-lib]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -171,7 +181,7 @@ 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-arm64-lib
|
||||||
path: ui/flutter/lib-arm64
|
path: ui/flutter/lib-arm64
|
||||||
@@ -183,15 +193,25 @@ jobs:
|
|||||||
|
|
||||||
# amd64 lib
|
# 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 -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/host github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||||
# universal binary
|
# universal binary
|
||||||
mkdir -p ui/flutter/macos/Frameworks
|
mkdir -p ui/flutter/macos/Frameworks
|
||||||
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
|
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-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
|
||||||
|
cp ui/flutter/lib-amd64/host ui/flutter/macos/Frameworks/amd64-host
|
||||||
|
cp ui/flutter/lib-arm64/host ui/flutter/macos/Frameworks/arm64-host
|
||||||
|
cp ui/flutter/lib-amd64/updater ui/flutter/macos/Frameworks/amd64-updater
|
||||||
|
cp ui/flutter/lib-arm64/updater ui/flutter/macos/Frameworks/arm64-updater
|
||||||
cd ui/flutter/macos/Frameworks
|
cd ui/flutter/macos/Frameworks
|
||||||
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
|
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
|
||||||
|
lipo -create -output host amd64-host arm64-host
|
||||||
|
lipo -create -output updater amd64-updater arm64-updater
|
||||||
|
rm -rf amd64-lib arm64-lib amd64-host arm64-host amd64-updater arm64-updater
|
||||||
|
mv host ../../assets/exec/host
|
||||||
|
|
||||||
cd $PROJECT_DIR/ui/flutter
|
cd $PROJECT_DIR/ui/flutter
|
||||||
flutter build macos
|
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||||
cd build/macos/Build/Products/Release
|
cd build/macos/Build/Products/Release
|
||||||
cat>appdmg.json<<EOF
|
cat>appdmg.json<<EOF
|
||||||
{
|
{
|
||||||
@@ -213,23 +233,34 @@ 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-latest
|
runs-on: ${{ matrix.os }}
|
||||||
needs: [ get-release ]
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, ubuntu-22.04-arm]
|
||||||
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: subosito/flutter-action@v2
|
- if: matrix.os == 'ubuntu-22.04'
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- if: matrix.os == 'ubuntu-22.04-arm'
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
channel: master
|
||||||
- run: |
|
- 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"
|
||||||
|
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
|
||||||
@@ -237,12 +268,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/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:
|
||||||
@@ -252,7 +313,7 @@ jobs:
|
|||||||
build-snap:
|
build-snap:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -267,41 +328,45 @@ 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/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
|
||||||
mkdir -p snap/gui
|
mkdir -p snap/gui
|
||||||
cp assets/icon/icon.svg snap/gui/gopeed.svg
|
cp assets/icon/icon.svg snap/gui/gopeed.svg
|
||||||
|
|
||||||
cat>snap/snapcraft.yaml<<EOF
|
cat>snap/snapcraft.yaml<<EOF
|
||||||
name: gopeed
|
name: gopeed
|
||||||
version: $VERSION
|
version: $VERSION
|
||||||
summary: A modern download manager
|
summary: A modern download manager
|
||||||
description: High speed downloader that supports all platforms.
|
description: High speed downloader that supports all platforms.
|
||||||
|
|
||||||
confinement: strict
|
confinement: strict
|
||||||
base: core22
|
base: core24
|
||||||
grade: stable
|
grade: stable
|
||||||
|
|
||||||
architectures:
|
platforms:
|
||||||
- build-on: amd64
|
amd64:
|
||||||
|
build-on: amd64
|
||||||
build-for: amd64
|
build-for: amd64
|
||||||
|
|
||||||
slots:
|
slots:
|
||||||
dbus-gopeed:
|
dbus-gopeed:
|
||||||
interface: dbus
|
interface: dbus
|
||||||
bus: session
|
bus: session
|
||||||
name: com.gopeed.gopeed
|
name: com.gopeed.gopeed
|
||||||
|
|
||||||
apps:
|
apps:
|
||||||
gopeed:
|
gopeed:
|
||||||
|
command-chain:
|
||||||
|
- bin/gpu-2404-wrapper
|
||||||
command: bin/gopeed
|
command: bin/gopeed
|
||||||
extensions: [gnome]
|
extensions: [gnome]
|
||||||
|
common-id: com.gopeed.Gopeed
|
||||||
plugs:
|
plugs:
|
||||||
- network
|
- network
|
||||||
- home
|
- home
|
||||||
slots:
|
|
||||||
- dbus-gopeed
|
|
||||||
parts:
|
parts:
|
||||||
flutter-git:
|
flutter-git:
|
||||||
source: https://github.com/flutter/flutter.git
|
source: https://github.com/flutter/flutter.git
|
||||||
@@ -341,14 +406,37 @@ 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:
|
||||||
|
after: [gopeed]
|
||||||
|
source: https://github.com/canonical/gpu-snap.git
|
||||||
|
plugin: dump
|
||||||
|
override-prime: |
|
||||||
|
craftctl default
|
||||||
|
\${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404
|
||||||
|
prime:
|
||||||
|
- bin/gpu-2404-wrapper
|
||||||
|
|
||||||
|
plugs:
|
||||||
|
gpu-2404:
|
||||||
|
interface: content
|
||||||
|
target: \$SNAP/gpu-2404
|
||||||
|
default-provider: mesa-2404
|
||||||
|
|
||||||
|
layout:
|
||||||
|
/usr/share/libdrm:
|
||||||
|
bind: \$SNAP/gpu-2404/libdrm
|
||||||
|
/usr/share/drirc.d:
|
||||||
|
symlink: \$SNAP/gpu-2404/drirc.d
|
||||||
|
/usr/share/X11/XErrorDB:
|
||||||
|
symlink: \$SNAP/gpu-2404/X11/XErrorDB
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
||||||
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
|
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
|
||||||
|
|
||||||
snapcraft --use-lxd
|
snapcraft --use-lxd
|
||||||
|
|
||||||
# Snapcraft login
|
# Snapcraft login
|
||||||
@@ -357,7 +445,7 @@ jobs:
|
|||||||
build-android:
|
build-android:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -366,7 +454,7 @@ jobs:
|
|||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
java-version: "11"
|
java-version: "17"
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
@@ -380,12 +468,16 @@ jobs:
|
|||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
go get golang.org/x/mobile/bind
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
gomobile init
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s -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:
|
||||||
@@ -395,7 +487,7 @@ jobs:
|
|||||||
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-latest
|
||||||
needs: [ get-release ]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -413,7 +505,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
|
||||||
@@ -426,9 +518,9 @@ jobs:
|
|||||||
asset_path: ui/flutter/dist/*
|
asset_path: ui/flutter/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-web:
|
build-web:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' || github.event.inputs.platform == 'qnap' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -442,11 +534,11 @@ jobs:
|
|||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --web-renderer html
|
flutter build web --no-web-resources-cdn
|
||||||
cd ../../
|
cd ../../
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
mkdir -p dist/zip
|
mkdir -p dist/zip
|
||||||
|
|
||||||
goos_arr=(windows darwin linux)
|
goos_arr=(windows darwin linux)
|
||||||
goarch_arr=(386 amd64 arm64)
|
goarch_arr=(386 amd64 arm64)
|
||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
@@ -468,16 +560,75 @@ jobs:
|
|||||||
|| true
|
|| true
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-dist
|
||||||
|
path: dist/zip/
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
asset_path: dist/zip/*
|
asset_path: dist/zip/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
build-qnap:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'qnap' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [get-release, build-web]
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.8.18"
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: web-dist
|
||||||
|
path: dist/zip/
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
|
run: |
|
||||||
|
sudo apt update -y
|
||||||
|
sudo apt install -y pv bsdmainutils
|
||||||
|
wget -O qdk2_0.32.bionic_amd64.deb "https://github.com/qnap-dev/qdk2/releases/download/v0.32/qdk2_0.32.bionic_amd64.deb"
|
||||||
|
dpkg -X qdk2_0.32.bionic_amd64.deb qdk2 # Direct installs will fail due to missing dependencies!
|
||||||
|
[[ -d qdk2 ]] || exit 1
|
||||||
|
|
||||||
|
export PATH=$(pwd)/qdk2/usr/bin:$(pwd)/qdk2/usr/share/qdk2/QDK/bin:${PATH}
|
||||||
|
wget -O Gopeed.template.tar.gz "https://github.com/GopeedLab/QpkgBuild/raw/refs/heads/master/template/Gopeed.template.tar.gz"
|
||||||
|
tar -zxf Gopeed.template.tar.gz
|
||||||
|
[[ -d Gopeed ]] || exit 1
|
||||||
|
|
||||||
|
goos=linux
|
||||||
|
goarch_arr=(amd64 arm64)
|
||||||
|
for goarch in "${goarch_arr[@]}"; do
|
||||||
|
qarch=x86_64
|
||||||
|
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
|
||||||
|
name=gopeed-web-v${VERSION}-${goos}-${goarch}
|
||||||
|
unzip dist/zip/${name}.zip -d dist/${name}
|
||||||
|
cp dist/${name}/${name}/* Gopeed/${qarch}/
|
||||||
|
done
|
||||||
|
cd Gopeed
|
||||||
|
sed -i -e 's/__QPKG_VER__/${VERSION}/g' qpkg.cfg
|
||||||
|
qbuild || exit 1
|
||||||
|
|
||||||
|
mkdir -p ../dist/qnap
|
||||||
|
goos=qnap
|
||||||
|
for goarch in "${goarch_arr[@]}"; do
|
||||||
|
qarch=x86_64
|
||||||
|
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
|
||||||
|
sname=Gopeed_${VERSION}_${qarch}.qpkg
|
||||||
|
dname=gopeed-v${VERSION}-${goos}-${goarch}.qpkg
|
||||||
|
[[ -f build/${sname} ]] && cp -ra build/${sname} ../dist/qnap/${dname}
|
||||||
|
done
|
||||||
|
- name: Upload
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
|
asset_path: dist/qnap/*
|
||||||
|
overwrite: true
|
||||||
build-docker:
|
build-docker:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- name: Remove unnecessary files
|
- name: Remove unnecessary files
|
||||||
run: |
|
run: |
|
||||||
@@ -499,7 +650,7 @@ jobs:
|
|||||||
- name: Build flutter web
|
- name: Build flutter web
|
||||||
run: |
|
run: |
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --web-renderer html
|
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.22"
|
GO_VERSION: "1.23"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
+99
-12
@@ -27,8 +27,8 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.22"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
@@ -53,16 +53,51 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Run Unit tests.
|
- name: Run Unit tests.
|
||||||
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
|
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/) -v -covermode=count -coverprofile=coverage.txt
|
||||||
- uses: codecov/codecov-action@v4
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||||
build:
|
build-desktop:
|
||||||
name: Build check
|
name: Build desktop check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
# needs: [lint, test]
|
strategy:
|
||||||
needs: [test]
|
matrix:
|
||||||
|
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
channel: master
|
||||||
|
- if: runner.os == 'Windows'
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build windows
|
||||||
|
- if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build macos
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build linux
|
||||||
|
build-mobile:
|
||||||
|
name: Build mobile check
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos-latest, ubuntu-latest ]
|
||||||
|
needs: [ test ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -72,10 +107,62 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
- if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ipa --no-codesign
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: "zulu"
|
||||||
|
java-version: "17"
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build apk
|
||||||
|
|
||||||
|
build-web:
|
||||||
|
name: Build web check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build linux
|
flutter build web --no-web-resources-cdn
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
go build -tags nosqlite,web -ldflags="-s -w" github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
build-docker:
|
||||||
|
name: Build docker check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web --no-web-resources-cdn
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
docker build -t gopeed .
|
||||||
+3
-4
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.22.6 AS go
|
FROM golang:1.23.3 AS go
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./go.mod ./go.sum ./
|
COPY ./go.mod ./go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
@@ -8,14 +8,13 @@ RUN CGO_ENABLED=0 go build -tags nosqlite,web \
|
|||||||
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
||||||
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
|
||||||
FROM alpine:3.14.2
|
FROM alpine:3.18
|
||||||
LABEL maintainer="monkeyWie"
|
LABEL maintainer="monkeyWie"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=go /app/dist/gopeed ./
|
COPY --from=go /app/dist/gopeed ./
|
||||||
COPY entrypoint.sh ./entrypoint.sh
|
COPY entrypoint.sh ./entrypoint.sh
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk upgrade --no-cache && \
|
apk add --no-cache su-exec ; \
|
||||||
apk add --no-cache bash su-exec; \
|
|
||||||
chmod +x ./entrypoint.sh && \
|
chmod +x ./entrypoint.sh && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
VOLUME ["/app/storage"]
|
VOLUME ["/app/storage"]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,86 +9,148 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Platform</th>
|
<td rowspan="2">🪟 Windows</td>
|
||||||
<th>Package Type</th>
|
<td><code>EXE</code></td>
|
||||||
<th>Download Link</th>
|
<td>amd64</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<td><code>Portable</code></td>
|
||||||
<td rowspan=2>Windows</td>
|
<td>amd64</td>
|
||||||
<td><code>EXE Installer</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>🍎 MacOS</td>
|
||||||
<td><code>Portable ZIP</code></td>
|
<td><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
<td>universal</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>MacOS</td>
|
<tr>
|
||||||
<td><code>DMG Installer</code></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
|
<td><code>Flathub</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
<td rowspan=4>Linux</td>
|
</tr>
|
||||||
<td><code>Flathub</code></td>
|
<tr>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
<td><code>SNAP</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
<td><code>SNAP</code></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<tr>
|
<td>amd64</td>
|
||||||
<td><code>DEB</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td><code>AppImage</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td>Android</td>
|
<td>amd64</td>
|
||||||
<td><code>APK</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td>iOS</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
<td><code>IPA</code></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<tr>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>Web</td>
|
<td>universal</td>
|
||||||
<td></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>armeabi-v7a</td>
|
||||||
<td>Docker</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
<td></td>
|
</tr>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
<tr>
|
||||||
</tr>
|
<td>arm64-v8a</td>
|
||||||
</tbody>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</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`:
|
||||||
|
|
||||||
@@ -94,40 +158,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.21+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
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
|
||||||
|
|
||||||
@@ -174,7 +238,7 @@ command:
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
@@ -200,18 +264,18 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
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)
|
||||||
+141
-76
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,115 +9,178 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Platform</th>
|
<td rowspan="2">🪟 Windows</td>
|
||||||
<th>Package Type</th>
|
<td><code>EXE</code></td>
|
||||||
<th>Download Link</th>
|
<td>amd64</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<td><code>Portable</code></td>
|
||||||
<td rowspan=2>Windows</td>
|
<td>amd64</td>
|
||||||
<td><code>EXE Installer</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>🍎 MacOS</td>
|
||||||
<td><code>Portable ZIP</code></td>
|
<td><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
<td>universal</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>MacOS</td>
|
<tr>
|
||||||
<td><code>DMG Installer</code></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
|
<td><code>Flathub</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
<td rowspan=4>Linux</td>
|
</tr>
|
||||||
<td><code>Flathub</code></td>
|
<tr>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
<td><code>SNAP</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
<td><code>SNAP</code></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<tr>
|
<td>amd64</td>
|
||||||
<td><code>DEB</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td><code>AppImage</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td>Android</td>
|
<td>amd64</td>
|
||||||
<td><code>APK</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td>iOS</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
<td><code>IPA</code></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<tr>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>Web</td>
|
<td>universal</td>
|
||||||
<td></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>armeabi-v7a</td>
|
||||||
<td>Docker</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
<td></td>
|
</tr>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
<tr>
|
||||||
</tr>
|
<td>arm64-v8a</td>
|
||||||
</tbody>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</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.22+
|
1. Go 言語 1.23+
|
||||||
2. Flutter 3.16+
|
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) をご参照ください
|
||||||
|
|
||||||
### ビルド
|
### 🏗️ ビルド
|
||||||
|
|
||||||
#### デスクトップ
|
#### デスクトップ
|
||||||
|
|
||||||
@@ -162,7 +227,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
@@ -188,7 +253,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感謝
|
## ❤️ 感謝
|
||||||
|
|
||||||
### コントリビューター
|
### コントリビューター
|
||||||
|
|
||||||
|
|||||||
+141
-78
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,86 +9,147 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nền tảng</th>
|
<td rowspan="2">🪟 Windows</td>
|
||||||
<th>Loại gói</th>
|
<td><code>EXE</code></td>
|
||||||
<th>Liên kết tải xuống</th>
|
<td>amd64</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<td><code>Portable</code></td>
|
||||||
<td rowspan=2>Windows</td>
|
<td>amd64</td>
|
||||||
<td><code>Bộ cài đặt EXE</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Liên kết</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>🍎 MacOS</td>
|
||||||
<td><code>ZIP Portable</code></td>
|
<td><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Liên kết</a></td>
|
<td>universal</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>MacOS</td>
|
<tr>
|
||||||
<td><code>Bộ cài đặt DMG</code></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Liên kết</a></td>
|
<td><code>Flathub</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
<td rowspan=4>Linux</td>
|
</tr>
|
||||||
<td><code>Flathub</code></td>
|
<tr>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Liên kết</a></td>
|
<td><code>SNAP</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
<td><code>SNAP</code></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">Liên kết</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<tr>
|
<td>amd64</td>
|
||||||
<td><code>DEB</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Liên kết</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td><code>AppImage</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Liên kết</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td>Android</td>
|
<td>amd64</td>
|
||||||
<td><code>APK</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Liên kết</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td>iOS</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
<td><code>IPA</code></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<tr>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>Web</td>
|
<td>universal</td>
|
||||||
<td></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Liên kết</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>armeabi-v7a</td>
|
||||||
<td>Docker</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
<td></td>
|
</tr>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Liên kết</a></td>
|
<tr>
|
||||||
</tr>
|
<td>arm64-v8a</td>
|
||||||
</tbody>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</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`:
|
||||||
|
|
||||||
@@ -94,36 +157,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.22+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
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
|
||||||
|
|
||||||
@@ -170,7 +233,7 @@ command:
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
@@ -196,7 +259,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
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
|
||||||
|
|
||||||
@@ -210,4 +273,4 @@ go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed
|
|||||||
|
|
||||||
## Giấy phép
|
## Giấy phép
|
||||||
|
|
||||||
[GPLv3](LICENSE)
|
[GPLv3](LICENSE)
|
||||||
+141
-78
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,86 +9,147 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>运行平台</th>
|
<td rowspan="2">🪟 Windows</td>
|
||||||
<th>安装包类型</th>
|
<td><code>EXE</code></td>
|
||||||
<th>下载链接</th>
|
<td>amd64</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<td><code>Portable</code></td>
|
||||||
<td rowspan=2>Windows</td>
|
<td>amd64</td>
|
||||||
<td><code>EXE Installer</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>🍎 MacOS</td>
|
||||||
<td><code>Portable ZIP</code></td>
|
<td><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
<td>universal</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>MacOS</td>
|
<tr>
|
||||||
<td><code>DMG Installer</code></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td>
|
<td><code>Flathub</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
<td rowspan=4>Linux</td>
|
</tr>
|
||||||
<td><code>Flathub</code></td>
|
<tr>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
|
<td><code>SNAP</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
<td><code>SNAP</code></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<tr>
|
<td>amd64</td>
|
||||||
<td><code>DEB</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td><code>AppImage</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td>Android</td>
|
<td>amd64</td>
|
||||||
<td><code>APK</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td>iOS</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
<td><code>IPA</code></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<tr>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>Web</td>
|
<td>universal</td>
|
||||||
<td></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>armeabi-v7a</td>
|
||||||
<td>Docker</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
<td></td>
|
</tr>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
<tr>
|
||||||
</tr>
|
<td>arm64-v8a</td>
|
||||||
</tbody>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
### 命令行工具
|
### 🛠️ 命令行工具
|
||||||
|
|
||||||
使用`go install`安装:
|
使用`go install`安装:
|
||||||
|
|
||||||
@@ -94,40 +157,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.22+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
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)
|
||||||
|
|
||||||
### 编译
|
### 🏗️ 编译
|
||||||
|
|
||||||
#### 桌面端
|
#### 桌面端
|
||||||
|
|
||||||
@@ -174,7 +237,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
@@ -200,7 +263,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感谢
|
## ❤️ 感谢
|
||||||
|
|
||||||
### 贡献者
|
### 贡献者
|
||||||
|
|
||||||
|
|||||||
+140
-76
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -7,86 +9,148 @@
|
|||||||
|
|
||||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[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>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>運行平台</th>
|
<td rowspan="2">🪟 Windows</td>
|
||||||
<th>安裝包類型</th>
|
<td><code>EXE</code></td>
|
||||||
<th>下載鏈接</th>
|
<td>amd64</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<td><code>Portable</code></td>
|
||||||
<td rowspan=2>Windows</td>
|
<td>amd64</td>
|
||||||
<td><code>EXE Installer</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>🍎 MacOS</td>
|
||||||
<td><code>Portable ZIP</code></td>
|
<td><code>DMG</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
<td>universal</td>
|
||||||
</tr>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>MacOS</td>
|
<tr>
|
||||||
<td><code>DMG Installer</code></td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td>
|
<td><code>Flathub</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
<td rowspan=4>Linux</td>
|
</tr>
|
||||||
<td><code>Flathub</code></td>
|
<tr>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
|
<td><code>SNAP</code></td>
|
||||||
</tr>
|
<td>amd64</td>
|
||||||
<tr>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
<td><code>SNAP</code></td>
|
</tr>
|
||||||
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<tr>
|
<td>amd64</td>
|
||||||
<td><code>DEB</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td><code>AppImage</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td>Android</td>
|
<td>amd64</td>
|
||||||
<td><code>APK</code></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>arm64</td>
|
||||||
<td>iOS</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
<td><code>IPA</code></td>
|
</tr>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
<tr>
|
||||||
</tr>
|
<td rowspan="4">🤖 Android</td>
|
||||||
<tr>
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>Web</td>
|
<td>universal</td>
|
||||||
<td></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>armeabi-v7a</td>
|
||||||
<td>Docker</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
<td></td>
|
</tr>
|
||||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
<tr>
|
||||||
</tr>
|
<td>arm64-v8a</td>
|
||||||
</tbody>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>📱 iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>🐳 Docker</td>
|
||||||
|
<td>-</td>
|
||||||
|
<td>universal</td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2">💾 Qnap</td>
|
||||||
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="8">🌐 Web</td>
|
||||||
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>386</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
### 使用CLI安裝
|
### 🛠️ 使用 CLI 安裝
|
||||||
|
|
||||||
使用`go install`安裝:
|
使用`go install`安裝:
|
||||||
|
|
||||||
@@ -94,36 +158,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.22+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
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)
|
||||||
|
|
||||||
### 編譯
|
### 🏗️ 編譯
|
||||||
|
|
||||||
#### 桌面端
|
#### 桌面端
|
||||||
|
|
||||||
@@ -170,7 +234,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
@@ -196,7 +260,7 @@ cp -r ui/flutter/build/web cmd/web/dist
|
|||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感謝
|
## ❤️ 感謝
|
||||||
|
|
||||||
### 貢獻者
|
### 貢獻者
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/browser"
|
||||||
|
"github.com/shirou/gopsutil/v4/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
const identifier = "gopeed"
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params json.RawMessage `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data any `json:"data,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
|
||||||
|
"ping": func(params json.RawMessage) (data any, err error) {
|
||||||
|
processes, err := process.Processes()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range processes {
|
||||||
|
name, err := p.Name()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
},
|
||||||
|
"create": func(params json.RawMessage) (data any, err error) {
|
||||||
|
var strParams string
|
||||||
|
if err = json.Unmarshal(params, &strParams); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
|
||||||
|
return
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
for {
|
||||||
|
// Read message length (first 4 bytes)
|
||||||
|
var length uint32
|
||||||
|
if err := binary.Read(os.Stdin, binary.NativeEndian, &length); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
// Connection closed by Chrome
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendError("Failed to read message length: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the message
|
||||||
|
input := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(os.Stdin, input); err != nil {
|
||||||
|
sendError("Failed to read message: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse message
|
||||||
|
var message Message
|
||||||
|
if err := json.Unmarshal(input, &message); err != nil {
|
||||||
|
sendError("Failed to parse message: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle request
|
||||||
|
var data any
|
||||||
|
var err error
|
||||||
|
if handler, ok := apiMap[message.Method]; ok {
|
||||||
|
data, err = handler(message.Params)
|
||||||
|
} else {
|
||||||
|
err = errors.New("Unknown method: " + message.Method)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
sendError(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sendResponse(0, data, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendResponse(code int, data interface{}, message string) {
|
||||||
|
response := Response{
|
||||||
|
Code: code,
|
||||||
|
Data: data,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode response
|
||||||
|
responseBytes, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
sendError("Failed to encode response: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write message length
|
||||||
|
binary.Write(os.Stdout, binary.NativeEndian, uint32(len(responseBytes)))
|
||||||
|
// Write message
|
||||||
|
os.Stdout.Write(responseBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendError(msg string) {
|
||||||
|
sendResponse(1, nil, msg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/browser"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pid := flag.Int("pid", 0, "PID of the process to update")
|
||||||
|
updateChannel := flag.String("channel", "", "Update channel")
|
||||||
|
packagePath := flag.String("asset", "", "Path to the package asset")
|
||||||
|
logPath := flag.String("log", "", "Log file path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *pid == 0 {
|
||||||
|
log.Println("Invalid PID")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if *updateChannel == "" {
|
||||||
|
log.Println("Invalid update channel")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *logPath != "" {
|
||||||
|
logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||||
|
if err == nil {
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
restart bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
|
||||||
|
log.Printf("Update failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the application
|
||||||
|
if restart {
|
||||||
|
browser.OpenURL("gopeed:///")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete package asset
|
||||||
|
if *packagePath != "" {
|
||||||
|
os.Remove(*packagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
|
||||||
|
killSignalChan := make(chan any, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-killSignalChan
|
||||||
|
|
||||||
|
if err = killProcess(pid); err != nil {
|
||||||
|
log.Printf("Failed to kill process: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = waitForProcessExit(pid); err != nil {
|
||||||
|
log.Printf("Failed to wait for process exit: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
appDir := filepath.Dir(os.Args[0])
|
||||||
|
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
|
||||||
|
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
|
||||||
|
return false, errors.Wrap(err, "failed to install package")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForProcessExit(pid int) error {
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
// On some systems, error is returned if process doesn't exist
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send null signal to test if process exists
|
||||||
|
err = process.Signal(syscall.Signal(0))
|
||||||
|
if err != nil {
|
||||||
|
// If error occurs, the process no longer exists
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("process %d still running after timeout", pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func killProcess(pid int) error {
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.Kill()
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
return true, installByDmg(killSignalChan, packagePath, destDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByDmg handles macOS dmg package installation
|
||||||
|
func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
// /Applications/Gopeed.app/Contents/MacOS -> /Applications
|
||||||
|
appPath := getParentDir(getParentDir(getParentDir(destDir)))
|
||||||
|
output, err := exec.Command("hdiutil", "attach", packagePath, "-nobrowse").Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mountPoint := ""
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
if strings.Contains(line, "/Volumes/") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) > 2 {
|
||||||
|
mountPoint = fields[len(fields)-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mountPoint == "" {
|
||||||
|
return fmt.Errorf("failed to get mount point")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detach the mounted DMG
|
||||||
|
defer exec.Command("hdiutil", "detach", mountPoint, "-quiet").Run()
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(filepath.Join(mountPoint, "*.app"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return fmt.Errorf("no .app found in dmg")
|
||||||
|
}
|
||||||
|
|
||||||
|
killSignalChan <- nil
|
||||||
|
|
||||||
|
// Copy the new app to the destination
|
||||||
|
// cp -Rf /Volumes/GoPeed/GoPeed.app /Applications
|
||||||
|
if err := exec.Command("cp", "-Rf", matches[0], appPath).Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent directory safely, handling trailing separators
|
||||||
|
func getParentDir(path string) string {
|
||||||
|
// Remove trailing separators if they exist
|
||||||
|
path = strings.TrimRight(path, string(filepath.Separator))
|
||||||
|
// Now get the parent directory
|
||||||
|
return filepath.Dir(path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
killSignalChan <- nil
|
||||||
|
switch updateChannel {
|
||||||
|
case "linuxDeb":
|
||||||
|
return true, installByDeb(packagePath)
|
||||||
|
case "linuxFlathub":
|
||||||
|
return true, installByFlathub()
|
||||||
|
case "linuxSnap":
|
||||||
|
return true, installBySnap()
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("unsupported update channel for Linux: %s", updateChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeInTerminal tries to execute a command in one of several common terminal emulators
|
||||||
|
func executeInTerminal(command string) error {
|
||||||
|
terminals := []string{
|
||||||
|
"gnome-terminal", // GNOME
|
||||||
|
"konsole", // KDE
|
||||||
|
"xfce4-terminal", // XFCE
|
||||||
|
"xterm", // X11
|
||||||
|
}
|
||||||
|
|
||||||
|
command = fmt.Sprintf(`echo "Starting update..." && echo "[CMD] %s" && %s`, command, command)
|
||||||
|
|
||||||
|
for _, term := range terminals {
|
||||||
|
if _, err := exec.LookPath(term); err == nil {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch term {
|
||||||
|
case "gnome-terminal", "xfce4-terminal":
|
||||||
|
cmd = exec.Command(term, "--", "bash", "-c", command)
|
||||||
|
case "konsole":
|
||||||
|
cmd = exec.Command(term, "-e", "bash", "-c", command)
|
||||||
|
case "xterm":
|
||||||
|
cmd = exec.Command(term, "-e", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no suitable terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal or xterm")
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByDeb installs the .deb package
|
||||||
|
func installByDeb(packagePath string) error {
|
||||||
|
command := fmt.Sprintf(`sudo dpkg -i "%s"`, packagePath)
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByFlathub updates the application via Flathub
|
||||||
|
func installByFlathub() error {
|
||||||
|
command := "flatpak update com.gopeed.Gopeed -y"
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installBySnap updates the application via Snap
|
||||||
|
func installBySnap() error {
|
||||||
|
command := "sudo snap refresh gopeed"
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
switch updateChannel {
|
||||||
|
case "windowsInstaller":
|
||||||
|
return false, installByInstaller(killSignalChan, packagePath, destDir)
|
||||||
|
default:
|
||||||
|
return true, installByPortable(killSignalChan, packagePath, destDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByInstaller extracts the installer from the zip file and runs it
|
||||||
|
func installByInstaller(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
// Create a temp directory for extraction
|
||||||
|
tempDir, err := os.MkdirTemp("", "gopeed_update")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Extract the zip file
|
||||||
|
reader, err := zip.OpenReader(packagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
// Find the installer file
|
||||||
|
var installerPath string
|
||||||
|
for _, file := range reader.File {
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file
|
||||||
|
path := filepath.Join(tempDir, file.Name)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
dstFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
srcFile.Close()
|
||||||
|
dstFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is likely an installer (.exe, .msi), save its path
|
||||||
|
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||||
|
if ext == ".exe" || ext == ".msi" {
|
||||||
|
installerPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if installerPath == "" {
|
||||||
|
return fmt.Errorf("no installer found in the update package")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the installer
|
||||||
|
cmd := exec.Command(installerPath)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
killSignalChan <- nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByPortable extracts the portable version to the destination directory
|
||||||
|
func installByPortable(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
killSignalChan <- nil
|
||||||
|
|
||||||
|
reader, err := zip.OpenReader(packagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
|
path := filepath.Join(destDir, file.Name)
|
||||||
|
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
os.MkdirAll(path, file.Mode())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
dstFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
srcFile.Close()
|
||||||
|
dstFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+14
-11
@@ -1,12 +1,15 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gopeed:
|
gopeed:
|
||||||
container_name: gopeed
|
container_name: gopeed
|
||||||
ports:
|
ports:
|
||||||
- "9999:9999" # HTTP port (host:container)
|
- 9999:9999 # HTTP port (host:container)
|
||||||
image: liwei2633/gopeed
|
environment:
|
||||||
volumes:
|
- PUID=0
|
||||||
- ~/gopeed/Downloads:/root/Downloads # mount download path
|
- PGID=0
|
||||||
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
- UMASK=022
|
||||||
restart: unless-stopped
|
volumes:
|
||||||
|
- ~/gopeed/Downloads:/app/Downloads # mount download path
|
||||||
|
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
||||||
|
restart: unless-stopped
|
||||||
|
image: liwei2633/gopeed
|
||||||
|
# command: -u Username -p Password # optional authentication
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
module github.com/GopeedLab/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.22
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
github.com/anacrolix/missinggo/v2 v2.8.0
|
||||||
github.com/anacrolix/torrent v1.56.1
|
github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/dop251/goja v0.0.0-20240806095544-3491d4a58fbe
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||||
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
|
||||||
@@ -16,11 +18,20 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.3.10
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/sync v0.8.0
|
golang.org/x/sync v0.9.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/purego v0.8.1 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -31,48 +42,48 @@ require (
|
|||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/chansync v0.5.1 // indirect
|
github.com/anacrolix/chansync v0.6.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.21.1 // indirect
|
github.com/anacrolix/dht/v2 v2.22.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.3.0 // indirect
|
github.com/anacrolix/envpprof v1.4.0 // indirect
|
||||||
github.com/anacrolix/generics v0.0.2 // indirect
|
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
||||||
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||||
github.com/anacrolix/log v0.15.2 // indirect
|
github.com/anacrolix/log v0.16.0 // indirect
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.1.1 // indirect
|
||||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
github.com/anacrolix/multiless v0.4.0 // indirect
|
||||||
github.com/anacrolix/stm v0.5.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.5.1 // indirect
|
github.com/anacrolix/sync v0.5.3 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.4 // indirect
|
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||||
github.com/anacrolix/utp v0.2.0 // indirect
|
github.com/anacrolix/utp v0.2.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.13.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.3.3 // 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
|
||||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||||
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||||
github.com/go-llsqlite/crawshaw v0.5.3 // indirect
|
github.com/go-llsqlite/crawshaw v0.5.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
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.2 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // 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
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
@@ -81,48 +92,48 @@ require (
|
|||||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pion/datachannel v1.5.8 // indirect
|
github.com/pion/datachannel v1.5.9 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||||
github.com/pion/ice/v2 v2.3.31 // indirect
|
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||||
github.com/pion/interceptor v0.1.29 // indirect
|
github.com/pion/interceptor v0.1.37 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.12 // indirect
|
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.14 // indirect
|
github.com/pion/rtcp v1.2.14 // indirect
|
||||||
github.com/pion/rtp v1.8.7 // indirect
|
github.com/pion/rtp v1.8.9 // indirect
|
||||||
github.com/pion/sctp v1.8.19 // indirect
|
github.com/pion/sctp v1.8.33 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.20 // indirect
|
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||||
github.com/pion/stun v0.6.1 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.9 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
github.com/pion/webrtc/v3 v3.2.50 // indirect
|
github.com/pion/webrtc/v4 v4.0.2 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||||
|
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.24.12
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
|
||||||
github.com/tidwall/btree v1.7.0 // indirect
|
github.com/tidwall/btree v1.7.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.3 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
golang.org/x/crypto v0.25.0 // indirect
|
golang.org/x/crypto v0.29.0 // indirect
|
||||||
golang.org/x/mod v0.19.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
golang.org/x/net v0.31.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.23.0 // indirect
|
golang.org/x/tools v0.27.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
modernc.org/libc v1.55.6 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.31.1 // indirect
|
modernc.org/sqlite v1.33.1 // indirect
|
||||||
zombiezen.com/go/sqlite v1.3.0 // indirect
|
zombiezen.com/go/sqlite v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -2,13 +2,15 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
GetConfig func(v any)
|
GetConfig func(v any)
|
||||||
ProxyConfig *base.DownloaderProxyConfig
|
GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
|
||||||
FileController
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
}
|
}
|
||||||
@@ -23,6 +25,7 @@ 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 },
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ type FetcherManager interface {
|
|||||||
Filters() []*SchemeFilter
|
Filters() []*SchemeFilter
|
||||||
// Build returns a new fetcher.
|
// Build returns a new fetcher.
|
||||||
Build() Fetcher
|
Build() Fetcher
|
||||||
|
// ParseName name displayed when the task is not yet resolved, parsed from the request URL
|
||||||
|
ParseName(u string) string
|
||||||
|
// AutoRename returns whether the fetcher need renaming the download file when has the same name file.
|
||||||
|
AutoRename() bool
|
||||||
|
|
||||||
// DefaultConfig returns the default configuration of the protocol.
|
// DefaultConfig returns the default configuration of the protocol.
|
||||||
DefaultConfig() any
|
DefaultConfig() any
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -72,7 +75,7 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||||
cfg.ListenPort = f.config.ListenPort
|
cfg.ListenPort = f.config.ListenPort
|
||||||
cfg.HTTPProxy = f.ctl.ProxyConfig.ToHandler()
|
cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
|
||||||
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||||
ClientBaseDir: cfg.DataDir,
|
ClientBaseDir: cfg.DataDir,
|
||||||
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||||
@@ -97,18 +100,21 @@ 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
|
||||||
if err := f.addTorrent(req, false); err != nil {
|
if err := f.addTorrent(req, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.updateRes()
|
f.updateRes()
|
||||||
f.meta.Req = req
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
if f.meta.Res != nil {
|
if f.meta.Res != nil {
|
||||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
torrentDirMap[f.meta.Res.Hash] = opts.Path
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -120,7 +126,7 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||||
ft.setTorrentDir(f.meta.FolderPath())
|
ft.setTorrentDir(f.meta.Opts.Path)
|
||||||
}
|
}
|
||||||
files := f.torrent.Files()
|
files := f.torrent.Files()
|
||||||
// If the user does not specify the file to download, all files will be downloaded by default
|
// If the user does not specify the file to download, all files will be downloaded by default
|
||||||
@@ -245,16 +251,18 @@ func (f *Fetcher) isDone() bool {
|
|||||||
|
|
||||||
func (f *Fetcher) updateRes() {
|
func (f *Fetcher) updateRes() {
|
||||||
res := &base.Resource{
|
res := &base.Resource{
|
||||||
Name: f.torrent.Name(),
|
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||||
Hash: f.torrent.InfoHash().String(),
|
Hash: f.torrent.InfoHash().String(),
|
||||||
}
|
}
|
||||||
f.torrent.PeerConns()
|
// Directory torrent
|
||||||
|
if f.torrent.Info().Length == 0 {
|
||||||
|
res.Name = f.torrent.Name()
|
||||||
|
}
|
||||||
for i, file := range f.torrent.Files() {
|
for i, file := range f.torrent.Files() {
|
||||||
res.Files[i] = &base.FileInfo{
|
res.Files[i] = &base.FileInfo{
|
||||||
Name: filepath.Base(file.DisplayPath()),
|
Name: filepath.Base(file.DisplayPath()),
|
||||||
Path: util.Dir(file.Path()),
|
Path: util.Dir(file.DisplayPath()),
|
||||||
Size: file.Length(),
|
Size: file.Length(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,7 +297,7 @@ func (f *Fetcher) doUpload(fromUpload bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
stats := f.torrent.Stats()
|
stats := f.torrentStats()
|
||||||
f.data.SeedBytes = lastData.SeedBytes + stats.BytesWrittenData.Int64()
|
f.data.SeedBytes = lastData.SeedBytes + stats.BytesWrittenData.Int64()
|
||||||
|
|
||||||
// Check is download complete, if not don't check and stop seeding
|
// Check is download complete, if not don't check and stop seeding
|
||||||
@@ -326,6 +334,17 @@ func (f *Fetcher) doUpload(fromUpload bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get torrent stats maybe panic, see https://github.com/anacrolix/torrent/issues/972
|
||||||
|
func (f *Fetcher) torrentStats() torrent.TorrentStats {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// ignore panic
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return f.torrent.Stats()
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) UploadedBytes() int64 {
|
func (f *Fetcher) UploadedBytes() int64 {
|
||||||
return f.data.SeedBytes
|
return f.data.SeedBytes
|
||||||
}
|
}
|
||||||
@@ -336,26 +355,40 @@ 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)
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(req.URL)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else if schema == "DATA" {
|
} else {
|
||||||
_, data := util.ParseDataUri(req.URL)
|
var reader io.Reader
|
||||||
buf := bytes.NewBuffer(data)
|
if schema == "FILE" {
|
||||||
|
fileUrl, _ := url.Parse(req.URL)
|
||||||
|
filePath := fileUrl.Path[1:]
|
||||||
|
reader, err = os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if schema == "DATA" {
|
||||||
|
_, data := util.ParseDataUri(req.URL)
|
||||||
|
reader = bytes.NewBuffer(data)
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(req.URL)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.(io.Closer).Close()
|
||||||
|
}
|
||||||
|
|
||||||
var metaInfo *metainfo.MetaInfo
|
var metaInfo *metainfo.MetaInfo
|
||||||
metaInfo, err = metainfo.Load(buf)
|
metaInfo, err = metainfo.Load(reader)
|
||||||
if err != nil {
|
// Hotfix for https://github.com/anacrolix/torrent/issues/992, ignore "expected EOF" error
|
||||||
|
// TODO remove this after the issue is fixed
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.torrent, err = client.AddTorrent(metaInfo)
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
} else {
|
|
||||||
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -458,6 +491,28 @@ func (fm *FetcherManager) Build() fetcher.Fetcher {
|
|||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
|
var name string
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
params := url.Query()
|
||||||
|
if params.Get("dn") != "" {
|
||||||
|
return params.Get("dn")
|
||||||
|
}
|
||||||
|
if params.Get("xt") != "" {
|
||||||
|
xt := strings.Split(params.Get("xt"), ":")
|
||||||
|
return xt[len(xt)-1]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (fm *FetcherManager) DefaultConfig() any {
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
return &config{
|
return &config{
|
||||||
ListenPort: 0,
|
ListenPort: 0,
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -33,7 +36,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
|
||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -69,33 +71,142 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
t.Run("Resolve Single File", func(t *testing.T) {
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
err := fetcher.Resolve(&base.Request{
|
||||||
Extra: bt.ReqExtra{
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
Trackers: []string{
|
Extra: bt.ReqExtra{
|
||||||
"udp://tracker.birkenwald.de:6969/announce",
|
Trackers: []string{
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
panic(err)
|
||||||
panic(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Size: 1466714112,
|
||||||
Size: 1466714112,
|
Range: true,
|
||||||
Range: true,
|
Files: []*base.FileInfo{
|
||||||
Files: []*base.FileInfo{
|
{
|
||||||
{
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Size: 1466714112,
|
||||||
Size: 1466714112,
|
},
|
||||||
},
|
},
|
||||||
},
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve Single File Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Resolve Multi Files", func(t *testing.T) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/test.torrent",
|
||||||
|
Extra: bt.ReqExtra{
|
||||||
|
Trackers: []string{
|
||||||
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &base.Resource{
|
||||||
|
Name: "test",
|
||||||
|
Size: 107484864,
|
||||||
|
Range: true,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "c.txt",
|
||||||
|
Path: "path",
|
||||||
|
Size: 98501754,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "b.txt",
|
||||||
|
Size: 8904996,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "a.txt",
|
||||||
|
Size: 78114,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve Multi Files Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/test.unclean.torrent",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Resolve file scheme Torrent", func(t *testing.T) {
|
||||||
|
file, _ := filepath.Abs("./testdata/test.unclean.torrent")
|
||||||
|
uri := "file:///" + file
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: uri,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
tests := []struct {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken url",
|
||||||
|
args: args{
|
||||||
|
u: "magnet://!@#%github.com",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dn",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed&dn=ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
},
|
||||||
|
want: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no dn",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
},
|
||||||
|
want: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non standard magnet",
|
||||||
|
args: args{
|
||||||
|
u: "magnet:?xxt=abcd",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||||
|
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +232,9 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher
|
|||||||
newController.GetConfig = func(v any) {
|
newController.GetConfig = func(v any) {
|
||||||
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
|
||||||
}
|
}
|
||||||
newController.ProxyConfig = proxyConfig
|
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
return proxyConfig.ToHandler()
|
||||||
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/anacrolix/torrent/storage"
|
"github.com/anacrolix/torrent/storage"
|
||||||
"io"
|
"io"
|
||||||
@@ -53,7 +54,7 @@ func (fs fileClientImpl) Close() error {
|
|||||||
return fs.opts.PieceCompletion.Close()
|
return fs.opts.PieceCompletion.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
func (fs fileClientImpl) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
||||||
upvertedFiles := info.UpvertedFiles()
|
upvertedFiles := info.UpvertedFiles()
|
||||||
files := make([]file, 0, len(upvertedFiles))
|
files := make([]file, 0, len(upvertedFiles))
|
||||||
for _, fileInfo := range upvertedFiles {
|
for _, fileInfo := range upvertedFiles {
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -3,10 +3,12 @@ package http
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -24,6 +26,9 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const connectTimeout = 15 * time.Second
|
||||||
|
const readTimeout = 15 * time.Second
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
Code int
|
Code int
|
||||||
Msg string
|
Msg string
|
||||||
@@ -42,9 +47,15 @@ type chunk struct {
|
|||||||
End int64
|
End int64
|
||||||
Downloaded int64
|
Downloaded int64
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -79,6 +90,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
f.meta.Req = req
|
||||||
httpReq, err := f.buildRequest(nil, req)
|
httpReq, err := f.buildRequest(nil, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -99,11 +111,11 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
||||||
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
|
// response 206 status code, support breakpoint continuation
|
||||||
res.Range = true
|
res.Range = true
|
||||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
// parse content length from Content-Range header, eg: bytes 0-1000/1001 or bytes 0-0/*
|
||||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||||
if contentTotal != "" {
|
if contentTotal != "" && contentTotal != "*" {
|
||||||
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -111,7 +123,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
res.Size = parse
|
res.Size = parse
|
||||||
}
|
}
|
||||||
} else if base.HttpCodeOK == httpResp.StatusCode {
|
} else if base.HttpCodeOK == httpResp.StatusCode {
|
||||||
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
// response 200 status code, not support breakpoint continuation, get file size by Content-Length header
|
||||||
|
// if not found, maybe chunked encoding
|
||||||
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
|
||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||||
@@ -140,19 +153,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 = filename
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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 == "." {
|
||||||
file.Name = httpReq.URL.Hostname()
|
file.Name = httpReq.URL.Hostname()
|
||||||
}
|
}
|
||||||
res.Files = append(res.Files, file)
|
res.Files = append(res.Files, file)
|
||||||
f.meta.Req = req
|
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -160,9 +183,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
func (f *Fetcher) Create(opts *base.Options) error {
|
func (f *Fetcher) Create(opts *base.Options) error {
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
|
|
||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -170,8 +190,12 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
|||||||
opts.Extra = &fhttp.OptsExtra{}
|
opts.Extra = &fhttp.OptsExtra{}
|
||||||
}
|
}
|
||||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||||
if extra.Connections == 0 {
|
if extra.Connections <= 0 {
|
||||||
extra.Connections = f.config.Connections
|
extra.Connections = f.config.Connections
|
||||||
|
// Avoid zero connections configuration
|
||||||
|
if extra.Connections <= 0 {
|
||||||
|
extra.Connections = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -192,6 +216,12 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid request extra modified by extension
|
||||||
|
if err = base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if f.chunks == nil {
|
if f.chunks == nil {
|
||||||
f.chunks = f.splitChunk()
|
f.chunks = f.splitChunk()
|
||||||
}
|
}
|
||||||
@@ -245,19 +275,34 @@ 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))
|
||||||
for i := 0; i < len(f.chunks); i++ {
|
for i := 0; i < len(f.chunks); i++ {
|
||||||
i := i
|
i := i
|
||||||
f.eg.Go(func() error {
|
f.eg.Go(func() error {
|
||||||
return f.fetchChunk(i, ctx)
|
err := f.fetchChunk(i, ctx)
|
||||||
|
// if canceled, fail fast
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chunkErrs[i] = err
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := f.eg.Wait()
|
err := f.eg.Wait()
|
||||||
// check if canceled
|
// error returned only if canceled, just return
|
||||||
if errors.Is(err, context.Canceled) {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// check all fetch results, if any error, return
|
||||||
|
for _, chunkErr := range chunkErrs {
|
||||||
|
if chunkErr != nil {
|
||||||
|
err = chunkErr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
f.file.Close()
|
f.file.Close()
|
||||||
// Update file last modified time
|
// Update file last modified time
|
||||||
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
||||||
@@ -269,34 +314,34 @@ func (f *Fetcher) fetch() {
|
|||||||
|
|
||||||
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||||
chunk := f.chunks[index]
|
chunk := f.chunks[index]
|
||||||
|
chunk.failed = false
|
||||||
chunk.retryTimes = 0
|
chunk.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
|
// retry until all remain chunks failed
|
||||||
for {
|
for {
|
||||||
// if chunk is completed, return
|
// if chunk is completed, return
|
||||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
if f.meta.Res.Range && chunk.remain() <= 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
// if all chunks failed, return
|
||||||
if chunk.retryTimes >= maxRetries {
|
if chunk.failed {
|
||||||
if !f.meta.Res.Range {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if all failed
|
|
||||||
allFailed := true
|
allFailed := true
|
||||||
for _, c := range f.chunks {
|
for _, c := range f.chunks {
|
||||||
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
if !c.failed {
|
||||||
allFailed = false
|
allFailed = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allFailed {
|
if allFailed {
|
||||||
return
|
if chunk.retryTimes >= 3 {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
chunk.retryTimes++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,16 +369,30 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Http request success, reset retry times
|
chunk.failed = false
|
||||||
chunk.retryTimes = 0
|
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := reader.Read(buf)
|
||||||
if n > 0 {
|
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)
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
chunk.Downloaded += int64(n)
|
chunk.Downloaded += int64(n)
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -342,7 +401,6 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If canceled, do not retry
|
// If canceled, do not retry
|
||||||
@@ -350,18 +408,10 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// retry request after 1 second
|
// retry request after 1 second
|
||||||
chunk.retryTimes = chunk.retryTimes + 1
|
chunk.failed = true
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// if chunk is completed, reset other not complete chunks retry times
|
|
||||||
if err == nil {
|
|
||||||
for _, c := range f.chunks {
|
|
||||||
if c != chunk && c.retryTimes > 0 {
|
|
||||||
c.retryTimes = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -410,6 +460,10 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
httpReq.Header = headers
|
httpReq.Header = headers
|
||||||
|
// Override Host header
|
||||||
|
if host := headers.Get(base.HttpHeaderHost); host != "" {
|
||||||
|
httpReq.Host = host
|
||||||
|
}
|
||||||
return httpReq, nil
|
return httpReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +497,13 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
|||||||
|
|
||||||
func (f *Fetcher) buildClient() *http.Client {
|
func (f *Fetcher) buildClient() *http.Client {
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
Proxy: f.ctl.ProxyConfig.ToHandler(),
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: connectTimeout,
|
||||||
|
}).DialContext,
|
||||||
|
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
// Cookie handle
|
// Cookie handle
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
@@ -481,6 +541,25 @@ func (fm *FetcherManager) Build() fetcher.Fetcher {
|
|||||||
return &Fetcher{}
|
return &Fetcher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) ParseName(u string) string {
|
||||||
|
var name string
|
||||||
|
url, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Get filePath by URL
|
||||||
|
name = path.Base(url.Path)
|
||||||
|
// If file name is empty, use host name
|
||||||
|
if name == "" || name == "/" || name == "." {
|
||||||
|
name = url.Hostname()
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fm *FetcherManager) AutoRename() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (fm *FetcherManager) DefaultConfig() any {
|
func (fm *FetcherManager) DefaultConfig() any {
|
||||||
return &config{
|
return &config{
|
||||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"net"
|
"net"
|
||||||
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -34,6 +37,26 @@ 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, "%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,
|
||||||
@@ -57,6 +80,21 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveWithHostHeader(t *testing.T) {
|
||||||
|
fetcher := buildFetcher()
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "https://bing.com",
|
||||||
|
Extra: &http.ReqExtra{
|
||||||
|
Header: map[string]string{
|
||||||
|
"Host": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "400") {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", err, "400")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
||||||
listener := startTestServer()
|
listener := startTestServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -130,6 +168,22 @@ func TestFetcher_DownloadLimit(t *testing.T) {
|
|||||||
downloadNormal(listener, 8, t)
|
downloadNormal(listener, 8, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
|
||||||
|
listener := test.StartTestLimitServer(16, readTimeout.Milliseconds()+5000)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadError(listener, 1, t)
|
||||||
|
downloadError(listener, 4, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadOnBugFileServer(t *testing.T) {
|
||||||
|
listener := test.StartTestRangeBugServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadNormal(listener, 1, t)
|
||||||
|
downloadNormal(listener, 4, t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadResume(t *testing.T) {
|
func TestFetcher_DownloadResume(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -192,6 +246,54 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken url",
|
||||||
|
args: args{
|
||||||
|
u: "https://!@#%github.com",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/index.html",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file path with query and hash",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com/a/b/index.html/#list?name=1",
|
||||||
|
},
|
||||||
|
want: "index.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no file path",
|
||||||
|
args: args{
|
||||||
|
u: "https://github.com",
|
||||||
|
},
|
||||||
|
want: "github.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fm := &FetcherManager{}
|
||||||
|
if got := fm.ParseName(tt.args.u); got != tt.want {
|
||||||
|
t.Errorf("ParseName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||||
return doDownloadReady(buildFetcher(), listener, connections, t)
|
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||||
}
|
}
|
||||||
@@ -337,10 +439,12 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||||
fetcher := downloadReady(httpListener, 4, t)
|
fetcher := downloadReady(httpListener, 4, t)
|
||||||
ctl := controller.NewController()
|
ctl := controller.NewController()
|
||||||
ctl.ProxyConfig = &base.DownloaderProxyConfig{
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
Enable: true,
|
return (&base.DownloaderProxyConfig{
|
||||||
Scheme: "socks5",
|
Enable: true,
|
||||||
Host: proxyListener.Addr().String(),
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
}).ToHandler()
|
||||||
}
|
}
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
@@ -359,11 +463,11 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildFetcher() *Fetcher {
|
func buildFetcher() *Fetcher {
|
||||||
fb := new(FetcherManager)
|
fm := new(FetcherManager)
|
||||||
fetcher := fb.Build()
|
fetcher := fm.Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
newController.GetConfig = func(v any) {
|
newController.GetConfig = func(v any) {
|
||||||
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
|
json.Unmarshal([]byte(test.ToJson(fm.DefaultConfig())), v)
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher.(*Fetcher)
|
return fetcher.(*Fetcher)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeoutReader struct {
|
||||||
|
reader io.Reader
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeoutReader(r io.Reader, timeout time.Duration) *TimeoutReader {
|
||||||
|
return &TimeoutReader{
|
||||||
|
reader: r,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *TimeoutReader) Read(p []byte) (n int, err error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), tr.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
var readErr error
|
||||||
|
var bytesRead int
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
bytesRead, readErr = tr.reader.Read(p)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return bytesRead, readErr
|
||||||
|
case <-ctx.Done():
|
||||||
|
return 0, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeoutReader_Read(t *testing.T) {
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||||
|
|
||||||
|
buf := make([]byte, len(data))
|
||||||
|
n, err := timeoutReader.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if n != len(data) {
|
||||||
|
t.Fatalf("expected to read %d bytes, read %d", len(data), n)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf, data) {
|
||||||
|
t.Fatalf("expected %s, got %s", data, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeoutReader_ReadTimeout(t *testing.T) {
|
||||||
|
reader := &slowReader{delay: 2 * time.Second}
|
||||||
|
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||||
|
|
||||||
|
buf := make([]byte, 8192)
|
||||||
|
_, err := timeoutReader.Read(buf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected timeout error, got nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
t.Fatalf("expected %v, got %v", context.DeadlineExceeded, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type slowReader struct {
|
||||||
|
delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *slowReader) Read(p []byte) (n int, err error) {
|
||||||
|
time.Sleep(sr.delay)
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
+130
-70
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/armon/go-socks5"
|
"github.com/armon/go-socks5"
|
||||||
@@ -36,7 +37,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func StartTestFileServer() net.Listener {
|
func StartTestFileServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
return http.FileServer(http.Dir(Dir))
|
return http.FileServer(http.Dir(Dir))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
return &SlowFileServer{
|
return &SlowFileServer{
|
||||||
delay: delay,
|
delay: delay,
|
||||||
handler: http.FileServer(http.Dir(Dir)),
|
handler: http.FileServer(http.Dir(Dir)),
|
||||||
@@ -61,7 +62,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestCustomServer() net.Listener {
|
func StartTestCustomServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
file, err := os.Open(BuildFile)
|
file, err := os.Open(BuildFile)
|
||||||
@@ -82,13 +83,32 @@ 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)
|
||||||
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartTestRetryServer() net.Listener {
|
func StartTestRetryServer() net.Listener {
|
||||||
counter := 0
|
counter := 0
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
@@ -108,7 +128,7 @@ func StartTestRetryServer() net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestPostServer() net.Listener {
|
func StartTestPostServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
||||||
@@ -132,7 +152,7 @@ func StartTestPostServer() net.Listener {
|
|||||||
|
|
||||||
func StartTestErrorServer() net.Listener {
|
func StartTestErrorServer() net.Listener {
|
||||||
counter := 0
|
counter := 0
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
@@ -149,7 +169,7 @@ func StartTestErrorServer() net.Listener {
|
|||||||
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||||
var connections atomic.Int32
|
var connections atomic.Int32
|
||||||
|
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -160,68 +180,105 @@ 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) {
|
||||||
file, err := os.Open(BuildFile)
|
slowCopyN(sl, writer, file, n, delay)
|
||||||
if err != nil {
|
},
|
||||||
panic(err)
|
)
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
slowCopy(writer, file, delay)
|
|
||||||
} else {
|
|
||||||
// split range
|
|
||||||
s := strings.Split(r, "=")
|
|
||||||
if len(s) != 2 {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s = strings.Split(s[1], "-")
|
|
||||||
if len(s) != 2 {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
start, err := strconv.ParseInt(s[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
end, err := strconv.ParseInt(s[1], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if start < 0 || end < 0 || start > end {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if end >= BuildSize {
|
|
||||||
end = BuildSize - 1
|
|
||||||
}
|
|
||||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
|
||||||
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
|
||||||
writer.Header().Set("Accept-Ranges", "bytes")
|
|
||||||
writer.WriteHeader(206)
|
|
||||||
file, err := os.Open(BuildFile)
|
|
||||||
if err != nil {
|
|
||||||
writer.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
file.Seek(start, 0)
|
|
||||||
slowCopyN(writer, file, end-start+1, delay)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return mux
|
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(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)
|
||||||
for {
|
for {
|
||||||
|
if sl.isShutdown {
|
||||||
|
return 0, errors.New("server shutdown")
|
||||||
|
}
|
||||||
nr, er := src.Read(buf)
|
nr, er := src.Read(buf)
|
||||||
if nr > 0 {
|
if nr > 0 {
|
||||||
nw, ew := dst.Write(buf[0:nr])
|
nw, ew := dst.Write(buf[0:nr])
|
||||||
@@ -250,8 +307,8 @@ func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err err
|
|||||||
return written, err
|
return written, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
func slowCopyN(sl *shutdownListener, dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||||
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
|
written, err = slowCopy(sl, dst, io.LimitReader(src, n), delay)
|
||||||
if written == n {
|
if written == n {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
@@ -262,7 +319,7 @@ func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int6
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.Listener {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -272,7 +329,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
// 随机生成一个文件
|
// Write random data
|
||||||
l := int64(8192)
|
l := int64(8192)
|
||||||
buf := make([]byte, l)
|
buf := make([]byte, l)
|
||||||
size := int64(0)
|
size := int64(0)
|
||||||
@@ -289,21 +346,24 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
size += l
|
size += l
|
||||||
}
|
}
|
||||||
server := &http.Server{}
|
server := &http.Server{}
|
||||||
server.Handler = serverHandle()
|
sl := &shutdownListener{
|
||||||
go server.Serve(listener)
|
|
||||||
|
|
||||||
return &shutdownListener{
|
|
||||||
server: server,
|
server: server,
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
}
|
}
|
||||||
|
server.Handler = serverHandle(sl)
|
||||||
|
go server.Serve(listener)
|
||||||
|
|
||||||
|
return sl
|
||||||
}
|
}
|
||||||
|
|
||||||
type shutdownListener struct {
|
type shutdownListener struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
|
isShutdown bool
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *shutdownListener) Close() error {
|
func (c *shutdownListener) Close() error {
|
||||||
|
c.isShutdown = true
|
||||||
closeErr := c.server.Shutdown(context.Background())
|
closeErr := c.server.Shutdown(context.Background())
|
||||||
if err := ifExistAndRemove(BuildFile); err != nil {
|
if err := ifExistAndRemove(BuildFile); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const (
|
|||||||
HttpCodeOK = 200
|
HttpCodeOK = 200
|
||||||
HttpCodePartialContent = 206
|
HttpCodePartialContent = 206
|
||||||
|
|
||||||
|
HttpHeaderHost = "Host"
|
||||||
HttpHeaderRange = "Range"
|
HttpHeaderRange = "Range"
|
||||||
HttpHeaderAcceptRanges = "Accept-Ranges"
|
HttpHeaderAcceptRanges = "Accept-Ranges"
|
||||||
HttpHeaderContentLength = "Content-Length"
|
HttpHeaderContentLength = "Content-Length"
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
NotFound = errors.New("not found")
|
|
||||||
BadParams = errors.New("bad params")
|
|
||||||
)
|
|
||||||
+48
-11
@@ -7,6 +7,7 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,6 +17,10 @@ type Request struct {
|
|||||||
Extra any `json:"extra"`
|
Extra any `json:"extra"`
|
||||||
// Labels is used to mark the download task
|
// Labels is used to mark the download task
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
|
// Proxy is special proxy config for request
|
||||||
|
Proxy *RequestProxy `json:"proxy"`
|
||||||
|
// SkipVerifyCert is the flag that skip verify cert
|
||||||
|
SkipVerifyCert bool `json:"skipVerifyCert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Validate() error {
|
func (r *Request) Validate() error {
|
||||||
@@ -25,6 +30,37 @@ func (r *Request) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestProxyMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RequestProxyModeFollow follow setting proxy
|
||||||
|
RequestProxyModeFollow RequestProxyMode = "follow"
|
||||||
|
// RequestProxyModeNone not use proxy
|
||||||
|
RequestProxyModeNone RequestProxyMode = "none"
|
||||||
|
// RequestProxyModeCustom custom proxy
|
||||||
|
RequestProxyModeCustom RequestProxyMode = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestProxy struct {
|
||||||
|
Mode RequestProxyMode `json:"mode"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Usr string `json:"usr"`
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||||
|
if p == nil || p.Mode != RequestProxyModeCustom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Scheme == "" || p.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd))
|
||||||
|
}
|
||||||
|
|
||||||
// Resource download resource
|
// Resource download resource
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
// if name is not empty, the resource is a folder and the name is the folder name
|
// if name is not empty, the resource is a folder and the name is the folder name
|
||||||
@@ -94,15 +130,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) Clone() *Options {
|
func (o *Options) Clone() *Options {
|
||||||
if o == nil {
|
return util.DeepClone(o)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &Options{
|
|
||||||
Name: o.Name,
|
|
||||||
Path: o.Path,
|
|
||||||
SelectFiles: o.SelectFiles,
|
|
||||||
Extra: o.Extra,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseReqExtra[E any](req *Request) error {
|
func ParseReqExtra[E any](req *Request) error {
|
||||||
@@ -196,7 +224,7 @@ func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cfg.System {
|
if cfg.System {
|
||||||
ieproxy.ReloadConf()
|
safeProxyReloadConf()
|
||||||
return ieproxy.GetProxyFunc()
|
return ieproxy.GetProxyFunc()
|
||||||
}
|
}
|
||||||
if cfg.Scheme == "" || cfg.Host == "" {
|
if cfg.Scheme == "" || cfg.Host == "" {
|
||||||
@@ -211,7 +239,7 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cfg.System {
|
if cfg.System {
|
||||||
ieproxy.ReloadConf()
|
safeProxyReloadConf()
|
||||||
static := ieproxy.GetConf().Static
|
static := ieproxy.GetConf().Static
|
||||||
if static.Active && len(static.Protocols) > 0 {
|
if static.Active && len(static.Protocols) > 0 {
|
||||||
// If only one protocol, use it
|
// If only one protocol, use it
|
||||||
@@ -237,6 +265,15 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
|||||||
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
|
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prcLock sync.Mutex
|
||||||
|
|
||||||
|
func safeProxyReloadConf() {
|
||||||
|
prcLock.Lock()
|
||||||
|
defer prcLock.Unlock()
|
||||||
|
|
||||||
|
ieproxy.ReloadConf()
|
||||||
|
}
|
||||||
|
|
||||||
func parseUrlSafe(rawUrl string) *url.URL {
|
func parseUrlSafe(rawUrl string) *url.URL {
|
||||||
u, err := url.Parse(rawUrl)
|
u, err := url.Parse(rawUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+237
-123
File diff suppressed because it is too large.
Load diff
+231
-16
@@ -82,7 +82,45 @@ func TestDownloader_Create(t *testing.T) {
|
|||||||
want := test.FileMd5(test.BuildFile)
|
want := test.FileMd5(test.BuildFile)
|
||||||
got := test.FileMd5(test.DownloadFile)
|
got := test.FileMd5(test.DownloadFile)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("Download() got = %v, want %v", got, want)
|
t.Errorf("Downloader_Create() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_CreateNotInWhite(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(&DownloaderConfig{
|
||||||
|
DownloadDirWhiteList: []string{"./downloads"},
|
||||||
|
})
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer downloader.Clear()
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
rr, err := downloader.Resolve(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
_, err = downloader.Create(rr.ID, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !strings.Contains(err.Error(), "white") {
|
||||||
|
t.Errorf("TestDownloader_CreateNotInWhite() got = %v, want %v", err.Error(), "not in white list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +133,7 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
downloader.DeleteByStatues(nil, true)
|
downloader.Delete(nil, true)
|
||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -159,21 +197,21 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
|
|
||||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||||
// No proxy
|
// No proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
// Disable proxy
|
// Disable proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Enable = false
|
proxyCfg.Enable = false
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Enable system proxy but not set proxy environment variable
|
// Enable system proxy but not set proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Enable proxy but error proxy environment variable
|
// Enable proxy but error proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
||||||
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
@@ -184,33 +222,50 @@ func TestDownloader_CreateWithProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Enable system proxy and set proxy environment variable
|
// Enable system proxy and set proxy environment variable
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
||||||
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
||||||
proxyCfg.System = true
|
proxyCfg.System = true
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Invalid proxy scheme
|
// Invalid proxy scheme
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Scheme = ""
|
proxyCfg.Scheme = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Invalid proxy host
|
// Invalid proxy host
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Host = ""
|
proxyCfg.Host = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Use proxy without auth
|
// Use proxy without auth
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
// Use proxy with auth
|
// Use proxy with auth
|
||||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
|
// Request proxy mode follow
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeFollow
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode none
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
reqProxy.Mode = base.RequestProxyModeNone
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
|
|
||||||
|
// Request proxy mode custom
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
|
||||||
|
return reqProxy
|
||||||
|
}, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
|
||||||
usr, pwd := "", ""
|
usr, pwd := "", ""
|
||||||
if auth {
|
if auth {
|
||||||
usr, pwd = "admin", "123"
|
usr, pwd = "admin", "123"
|
||||||
@@ -223,17 +278,29 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer downloader.Clear()
|
defer downloader.Clear()
|
||||||
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&base.DownloaderProxyConfig{
|
globalProxyCfg := &base.DownloaderProxyConfig{
|
||||||
Enable: true,
|
Enable: true,
|
||||||
Scheme: "socks5",
|
Scheme: "socks5",
|
||||||
Host: proxyListener.Addr().String(),
|
Host: proxyListener.Addr().String(),
|
||||||
Usr: usr,
|
Usr: usr,
|
||||||
Pwd: pwd,
|
Pwd: pwd,
|
||||||
})
|
}
|
||||||
|
if buildProxyConfig != nil {
|
||||||
|
globalProxyCfg = buildProxyConfig(globalProxyCfg)
|
||||||
|
}
|
||||||
|
downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg
|
||||||
|
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: test.ExternalDownloadUrl,
|
URL: test.ExternalDownloadUrl,
|
||||||
}
|
}
|
||||||
|
if buildReqProxy != nil {
|
||||||
|
req.Proxy = buildReqProxy(&base.RequestProxy{
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
})
|
||||||
|
}
|
||||||
rr, err := downloader.Resolve(req)
|
rr, err := downloader.Resolve(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errHandler == nil {
|
if errHandler == nil {
|
||||||
@@ -333,7 +400,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 1001)
|
time.Sleep(time.Millisecond * 1001)
|
||||||
err = downloader.Pause(id)
|
err = downloader.Pause(&TaskFilter{IDs: []string{id}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -357,7 +424,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
|
|||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
err = downloader.Continue(id)
|
err = downloader.Continue(&TaskFilter{IDs: []string{id}})
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -418,3 +485,151 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
|||||||
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
|
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_GetTasksByFilter(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(nil)
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
downloader.Delete(nil, true)
|
||||||
|
downloader.Clear()
|
||||||
|
}()
|
||||||
|
|
||||||
|
reqs := make([]*base.Request, 0)
|
||||||
|
fileNames := make([]string, 0)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
reqs = append(reqs, req)
|
||||||
|
if i == 0 {
|
||||||
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
|
} else {
|
||||||
|
arr := strings.Split(test.DownloadName, ".")
|
||||||
|
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(reqs))
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyDone {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
||||||
|
Path: test.Dir,
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter nil", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(nil)
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter nil task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter empty", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter empty task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: taskIds,
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
})
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match ids", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{"xxx"},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
Statuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
Statuses: []base.Status{base.DownloadStatusError},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match notStatus", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
NotStatuses: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
|
||||||
|
})
|
||||||
|
if len(tasks) != len(reqs) {
|
||||||
|
t.Errorf("GetTasksByFilter match notStatus task got = %v, want %v", len(tasks), len(reqs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match notStatus", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
NotStatuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter not match notStatus task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter match ids and status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
Statuses: []base.Status{base.DownloadStatusDone},
|
||||||
|
})
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Errorf("GetTasksByFilter match ids and status task got = %v, want %v", len(tasks), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTasksByFilter not match ids and status", func(t *testing.T) {
|
||||||
|
tasks := downloader.GetTasksByFilter(&TaskFilter{
|
||||||
|
IDs: []string{taskIds[0]},
|
||||||
|
Statuses: []base.Status{base.DownloadStatusError},
|
||||||
|
})
|
||||||
|
if len(tasks) != 0 {
|
||||||
|
t.Errorf("GetTasksByFilter not match ids and status task got = %v, want %v", len(tasks), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ func (e *Engine) await(value any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) Close() {
|
func (e *Engine) Close() {
|
||||||
e.loop.Stop()
|
e.loop.StopNoWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const (
|
|||||||
EventOnResolve ActivationEvent = "onResolve"
|
EventOnResolve ActivationEvent = "onResolve"
|
||||||
EventOnStart ActivationEvent = "onStart"
|
EventOnStart ActivationEvent = "onStart"
|
||||||
EventOnError ActivationEvent = "onError"
|
EventOnError ActivationEvent = "onError"
|
||||||
//EventOnDone ActivationEvent = "onDone"
|
EventOnDone ActivationEvent = "onDone"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
|
||||||
@@ -304,6 +304,17 @@ func (d *Downloader) triggerOnError(task *Task, err error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) triggerOnDone(task *Task) {
|
||||||
|
doTrigger(d,
|
||||||
|
EventOnDone,
|
||||||
|
task.Meta.Req,
|
||||||
|
&OnErrorContext{
|
||||||
|
Task: NewExtensionTask(d, task),
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
|
||||||
// init extension global object
|
// init extension global object
|
||||||
gopeed := &Instance{
|
gopeed := &Instance{
|
||||||
@@ -611,9 +622,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
|
|||||||
h.register(EventOnError, fn)
|
h.register(EventOnError, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (h InstanceEvents) OnDone(fn goja.Callable) {
|
func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||||
// h.register(HookEventOnDone, fn)
|
h.register(EventOnDone, fn)
|
||||||
//}
|
}
|
||||||
|
|
||||||
type ExtensionInfo struct {
|
type ExtensionInfo struct {
|
||||||
Identity string `json:"identity"`
|
Identity string `json:"identity"`
|
||||||
@@ -687,25 +698,39 @@ type OnErrorContext struct {
|
|||||||
Error error `json:"error"`
|
Error error `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtensionTask struct {
|
type OnDoneContext struct {
|
||||||
*Task
|
Task *Task `json:"task"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionTask is a wrapper of Task, it's used to interact with extension scripts.
|
||||||
|
// Avoid extension scripts modifying task directly, use ExtensionTask to encapsulate task,
|
||||||
|
// only some fields can be modified, such as request info.
|
||||||
|
type ExtensionTask struct {
|
||||||
download *Downloader
|
download *Downloader
|
||||||
|
|
||||||
|
*Task
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||||
|
// restricts extension scripts to only modify request info
|
||||||
|
newTask := task.clone()
|
||||||
|
newTask.Meta.Req = task.Meta.Req
|
||||||
return &ExtensionTask{
|
return &ExtensionTask{
|
||||||
Task: task.clone(),
|
|
||||||
download: download,
|
download: download,
|
||||||
|
Task: newTask,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExtensionTask) Continue() error {
|
func (t *ExtensionTask) Continue() error {
|
||||||
return t.download.Continue(t.ID)
|
return t.download.Continue(&TaskFilter{
|
||||||
|
IDs: []string{t.ID},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ExtensionTask) Pause() error {
|
func (t *ExtensionTask) Pause() error {
|
||||||
return t.download.Pause(t.ID)
|
return t.download.Pause(&TaskFilter{
|
||||||
|
IDs: []string{t.ID},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSettings(settings []*Setting) map[string]any {
|
func parseSettings(settings []*Setting) map[string]any {
|
||||||
|
|||||||
@@ -247,8 +247,45 @@ func TestDownloader_Extension_OnError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// extension on error modify url and continue download
|
// extension on error modify url and continue download
|
||||||
task := downloader.GetTask(id)
|
task := downloader.GetTask(id)
|
||||||
if task.Meta.Req.URL != "https://github.com" {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_Extension_OnDone(t *testing.T) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_done", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
downloader.Listener(func(event *Event) {
|
||||||
|
if event.Key == EventKeyFinally {
|
||||||
|
errCh <- event.Err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
id, err := downloader.CreateDirect(&base.Request{
|
||||||
|
URL: "https://github.com",
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-errCh:
|
||||||
|
break
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
err = errors.New("timeout")
|
||||||
|
}
|
||||||
|
// wait for script execution
|
||||||
|
time.Sleep(time.Millisecond * 3000)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("extension on done download error: " + err.Error())
|
||||||
|
}
|
||||||
|
// extension on error modify url and continue download
|
||||||
|
task := downloader.GetTask(id)
|
||||||
|
if task.Meta.Req.Labels["modified"] != "true" {
|
||||||
|
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
|
||||||
}
|
}
|
||||||
if task.Status != base.DownloadStatusDone {
|
if task.Status != base.DownloadStatusDone {
|
||||||
t.Fatalf("except status is done, actual: %s", task.Status)
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
|||||||
+74
-19
@@ -1,6 +1,7 @@
|
|||||||
package download
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"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/internal/protocol/bt"
|
"github.com/GopeedLab/gopeed/internal/protocol/bt"
|
||||||
@@ -49,43 +50,97 @@ func NewTask() *Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the display name of the task.
|
||||||
|
func (t *Task) Name() string {
|
||||||
|
// Custom name first
|
||||||
|
if t.Meta.Opts.Name != "" {
|
||||||
|
return t.Meta.Opts.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task is not resolved, parse the name from the URL
|
||||||
|
if t.Meta.Res == nil {
|
||||||
|
fallbackName := "unknown"
|
||||||
|
if t.fetcherManager == nil {
|
||||||
|
return fallbackName
|
||||||
|
}
|
||||||
|
parseName := t.fetcherManager.ParseName(t.Meta.Req.URL)
|
||||||
|
if parseName == "" {
|
||||||
|
return fallbackName
|
||||||
|
}
|
||||||
|
return parseName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task is a folder
|
||||||
|
if t.Meta.Res.Name != "" {
|
||||||
|
return t.Meta.Res.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the name of the first file
|
||||||
|
return t.Meta.Res.Files[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) MarshalJSON() ([]byte, error) {
|
||||||
|
type rawTaskType Task
|
||||||
|
jsonTask := struct {
|
||||||
|
rawTaskType
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{
|
||||||
|
rawTaskType(*t),
|
||||||
|
t.Name(),
|
||||||
|
}
|
||||||
|
return json.Marshal(jsonTask)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) updateStatus(status base.Status) {
|
func (t *Task) updateStatus(status base.Status) {
|
||||||
t.UpdatedAt = time.Now()
|
t.UpdatedAt = time.Now()
|
||||||
t.Status = status
|
t.Status = status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) clone() *Task {
|
func (t *Task) clone() *Task {
|
||||||
return &Task{
|
return util.DeepClone(t)
|
||||||
ID: t.ID,
|
|
||||||
Protocol: t.Protocol,
|
|
||||||
Meta: t.Meta,
|
|
||||||
Status: t.Status,
|
|
||||||
Uploading: t.Uploading,
|
|
||||||
Progress: t.Progress,
|
|
||||||
CreatedAt: t.CreatedAt,
|
|
||||||
UpdatedAt: t.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
IDs []string
|
||||||
|
Statuses []base.Status
|
||||||
|
NotStatuses []base.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TaskFilter) IsEmpty() bool {
|
||||||
|
return len(f.IDs) == 0 && len(f.Statuses) == 0 && len(f.NotStatuses) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloaderConfig struct {
|
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 `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||||
Storage Storage
|
Storage Storage
|
||||||
StorageDir string
|
StorageDir string
|
||||||
|
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
gopeed.events.onDone(async function (ctx) {
|
||||||
|
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||||
|
ctx.task.meta.req.labels['modified'] = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "on-done",
|
||||||
|
"title": "gopeed extension on done event test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onDone",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+75
-29
@@ -74,13 +74,27 @@ func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
func PauseTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
if taskId == "" {
|
WriteJson(w, errResult)
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Pause(taskId); err != nil {
|
|
||||||
|
if err := Downloader.Pause(filter); err != nil {
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJson(w, model.NewNilResult())
|
||||||
|
}
|
||||||
|
|
||||||
|
func PauseTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filter, errResult := parseFilter(r)
|
||||||
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Downloader.Pause(filter); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -88,29 +102,27 @@ func PauseTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
if taskId == "" {
|
WriteJson(w, errResult)
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Continue(taskId); err != nil {
|
|
||||||
|
if err := Downloader.Continue(filter); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WriteJson(w, model.NewNilResult())
|
WriteJson(w, model.NewNilResult())
|
||||||
}
|
}
|
||||||
|
|
||||||
func PauseAllTask(w http.ResponseWriter, r *http.Request) {
|
func ContinueTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := Downloader.PauseAll(); err != nil {
|
filter, errResult := parseFilter(r)
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
WriteJson(w, model.NewNilResult())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
|
if err := Downloader.Continue(filter); err != nil {
|
||||||
if err := Downloader.ContinueAll(); err != nil {
|
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -118,14 +130,14 @@ func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
filter, errResult := parseIdFilter(r)
|
||||||
taskId := vars["id"]
|
if errResult != nil {
|
||||||
force := r.FormValue("force")
|
WriteJson(w, errResult)
|
||||||
if taskId == "" {
|
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := Downloader.Delete(taskId, force == "true"); err != nil {
|
force := r.FormValue("force")
|
||||||
|
|
||||||
|
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -133,10 +145,14 @@ func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
filter, errResult := parseFilter(r)
|
||||||
status := r.Form["status"]
|
if errResult != nil {
|
||||||
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
force := r.FormValue("force")
|
force := r.FormValue("force")
|
||||||
if err := Downloader.DeleteByStatues(convertStatues(status), force == "true"); err != nil {
|
|
||||||
|
if err := Downloader.Delete(filter, force == "true"); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,9 +175,13 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
filter, errResult := parseFilter(r)
|
||||||
status := r.Form["status"]
|
if errResult != nil {
|
||||||
tasks := Downloader.GetTasksByStatues(convertStatues(status))
|
WriteJson(w, errResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := Downloader.GetTasksByFilter(filter)
|
||||||
WriteJson(w, model.NewOkResult(tasks))
|
WriteJson(w, model.NewOkResult(tasks))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +344,32 @@ func GetStats(w http.ResponseWriter, r *http.Request) {
|
|||||||
WriteJson(w, model.NewOkResult(statsResult))
|
WriteJson(w, model.NewOkResult(statsResult))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseIdFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
taskId := vars["id"]
|
||||||
|
if taskId == "" {
|
||||||
|
return nil, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &download.TaskFilter{
|
||||||
|
IDs: []string{taskId},
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFilter(r *http.Request) (*download.TaskFilter, any) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return nil, model.NewErrorResult(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &download.TaskFilter{
|
||||||
|
IDs: r.Form["id"],
|
||||||
|
Statuses: convertStatues(r.Form["status"]),
|
||||||
|
NotStatuses: convertStatues(r.Form["notStatus"]),
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
func convertStatues(statues []string) []base.Status {
|
func convertStatues(statues []string) []base.Status {
|
||||||
result := make([]base.Status, 0)
|
result := make([]base.Status, 0)
|
||||||
for _, status := range statues {
|
for _, status := range statues {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -14,13 +14,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StartConfig struct {
|
type StartConfig struct {
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
RefreshInterval int `json:"refreshInterval"`
|
RefreshInterval int `json:"refreshInterval"`
|
||||||
Storage Storage `json:"storage"`
|
Storage Storage `json:"storage"`
|
||||||
StorageDir string `json:"storageDir"`
|
StorageDir string `json:"storageDir"`
|
||||||
ApiToken string `json:"apiToken"`
|
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
ApiToken string `json:"apiToken"`
|
||||||
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
+83
-7
@@ -10,12 +10,14 @@ import (
|
|||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -104,9 +106,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
|
||||||
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseTasks)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
|
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueTasks)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
|
|
||||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
|
||||||
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
|
||||||
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
|
||||||
@@ -126,16 +128,28 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
if startCfg.WebEnable {
|
if startCfg.WebEnable {
|
||||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
||||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||||
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
|
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
|
enableApiToken := startCfg.ApiToken != ""
|
||||||
|
enableBasicAuth := startCfg.WebEnable && startCfg.WebBasicAuth != nil
|
||||||
|
if enableApiToken || enableBasicAuth {
|
||||||
r.Use(func(h http.Handler) http.Handler {
|
r.Use(func(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
|
if enableApiToken {
|
||||||
h.ServeHTTP(w, r)
|
apiTokenHeader := r.Header["X-Api-Token"]
|
||||||
return
|
// If api token header is set, only check api token ignore basic auth
|
||||||
|
if len(apiTokenHeader) > 0 {
|
||||||
|
if apiTokenHeader[0] == startCfg.ApiToken {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
||||||
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
@@ -222,6 +236,68 @@ func (e *extensionFileSystem) Open(name string) (http.File, error) {
|
|||||||
return os.Open(filepath.Join(extensionPath, path))
|
return os.Open(filepath.Join(extensionPath, path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type embedCacheFileSystem struct {
|
||||||
|
fs http.FileSystem
|
||||||
|
lastModTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEmbedCacheFileSystem(fs http.FileSystem) *embedCacheFileSystem {
|
||||||
|
efs := &embedCacheFileSystem{
|
||||||
|
fs: fs,
|
||||||
|
lastModTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return efs
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Stat(exe)
|
||||||
|
if err != nil {
|
||||||
|
return efs
|
||||||
|
}
|
||||||
|
|
||||||
|
efs.lastModTime = fi.ModTime()
|
||||||
|
return efs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embedCacheFileSystem) Open(name string) (http.File, error) {
|
||||||
|
file, err := e.fs.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &embedFile{
|
||||||
|
File: file,
|
||||||
|
lastModTime: e.lastModTime,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type embedFile struct {
|
||||||
|
http.File
|
||||||
|
lastModTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type embedFileInfo struct {
|
||||||
|
fs.FileInfo
|
||||||
|
lastModTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embedFileInfo) ModTime() time.Time {
|
||||||
|
return e.lastModTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embedFile) Stat() (fs.FileInfo, error) {
|
||||||
|
fi, err := e.File.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &embedFileInfo{
|
||||||
|
FileInfo: fi,
|
||||||
|
lastModTime: e.lastModTime,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
||||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
|||||||
+98
-3
@@ -14,6 +14,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -478,6 +479,66 @@ func TestFsExtensionFail(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebFsEnhance(t *testing.T) {
|
||||||
|
indexHtml := `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>index</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>index</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
webDistPath := "dist"
|
||||||
|
os.MkdirAll("dist", os.ModePerm)
|
||||||
|
if err := os.WriteFile(filepath.Join(webDistPath, "index.html"), []byte(indexHtml), os.ModePerm); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(webDistPath)
|
||||||
|
|
||||||
|
doTest0(func(cfg *model.StartConfig) {
|
||||||
|
cfg.WebFS = os.DirFS(webDistPath)
|
||||||
|
}, func() {
|
||||||
|
// First request no cache
|
||||||
|
code, header, _ := doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
}, nil)
|
||||||
|
if code != http.StatusOK {
|
||||||
|
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
|
||||||
|
}
|
||||||
|
// Check header last-modified
|
||||||
|
if _, ok := header["Last-Modified"]; !ok {
|
||||||
|
t.Errorf("TestWebFsEnhance() missing key = %v", "Last-Modified")
|
||||||
|
}
|
||||||
|
// Check gzip compress
|
||||||
|
if _, ok := header["Content-Encoding"]; !ok || header["Content-Encoding"] != "gzip" {
|
||||||
|
t.Errorf("TestWebFsEnhance() no gzip compress")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request with If-Modified-Since
|
||||||
|
ifModifiedSince := header["Last-Modified"]
|
||||||
|
code, _, _ = doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
|
||||||
|
"If-Modified-Since": ifModifiedSince,
|
||||||
|
}, nil)
|
||||||
|
if code != http.StatusNotModified {
|
||||||
|
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusNotModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request with un gzip
|
||||||
|
code, header, _ = doHttpRequest1(http.MethodGet, "/index.html?t=123", nil, nil)
|
||||||
|
if code != http.StatusOK {
|
||||||
|
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
|
||||||
|
}
|
||||||
|
// Check no gzip compress
|
||||||
|
if _, ok := header["Content-Encoding"]; ok && header["Content-Encoding"] == "gzip" {
|
||||||
|
t.Errorf("TestWebFsEnhance() has gzip compress")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDoProxy(t *testing.T) {
|
func TestDoProxy(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
||||||
@@ -564,15 +625,38 @@ func TestAuthorization(t *testing.T) {
|
|||||||
if status != http.StatusOK {
|
if status != http.StatusOK {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": cfg.WebBasicAuth.Authorization(),
|
||||||
|
"X-Api-Token": cfg.ApiToken,
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusOK {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": cfg.WebBasicAuth.Authorization(),
|
||||||
|
"X-Api-Token": "",
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func doTest(handler func()) {
|
func doTest(handler func()) {
|
||||||
|
doTest0(nil, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTest0(onStart func(cfg *model.StartConfig), handler func()) {
|
||||||
testFunc := func(storage model.Storage) {
|
testFunc := func(storage model.Storage) {
|
||||||
var cfg = &model.StartConfig{}
|
var cfg = &model.StartConfig{}
|
||||||
cfg.Init()
|
cfg.Init()
|
||||||
cfg.Storage = storage
|
cfg.Storage = storage
|
||||||
cfg.StorageDir = ".test_storage"
|
cfg.StorageDir = ".test_storage"
|
||||||
cfg.WebEnable = true
|
cfg.WebEnable = true
|
||||||
|
if onStart != nil {
|
||||||
|
onStart(cfg)
|
||||||
|
}
|
||||||
fileListener := doStart(cfg)
|
fileListener := doStart(cfg)
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := fileListener.Close(); err != nil {
|
if err := fileListener.Close(); err != nil {
|
||||||
@@ -582,8 +666,8 @@ func doTest(handler func()) {
|
|||||||
Downloader.Clear()
|
Downloader.Clear()
|
||||||
}()
|
}()
|
||||||
defer func() {
|
defer func() {
|
||||||
Downloader.PauseAll()
|
Downloader.Pause(nil)
|
||||||
Downloader.DeleteByStatues(nil, true)
|
Downloader.Delete(nil, true)
|
||||||
os.RemoveAll(cfg.StorageDir)
|
os.RemoveAll(cfg.StorageDir)
|
||||||
}()
|
}()
|
||||||
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||||
@@ -603,6 +687,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 +712,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]) {
|
||||||
|
|||||||
@@ -12,3 +12,17 @@ func MapToStruct(s any, v any) error {
|
|||||||
}
|
}
|
||||||
return json.Unmarshal(b, v)
|
return json.Unmarshal(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeepClone[T any](v *T) *T {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var t T
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
json.Unmarshal(b, &t)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeepClone(t *testing.T) {
|
||||||
|
type user struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
|
||||||
|
v int
|
||||||
|
}
|
||||||
|
|
||||||
|
type args[T any] struct {
|
||||||
|
v *T
|
||||||
|
}
|
||||||
|
type testCase[T any] struct {
|
||||||
|
name string
|
||||||
|
args args[T]
|
||||||
|
want *T
|
||||||
|
}
|
||||||
|
tests := []testCase[user]{
|
||||||
|
{
|
||||||
|
name: "case 1",
|
||||||
|
args: args[user]{
|
||||||
|
v: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case 2",
|
||||||
|
args: args[user]{
|
||||||
|
v: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
v: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user{
|
||||||
|
Name: "test",
|
||||||
|
Age: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := DeepClone(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("DeepClone() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
func ParseSchema(url string) string {
|
func ParseSchema(url string) string {
|
||||||
index := strings.Index(url, ":")
|
index := strings.Index(url, ":")
|
||||||
// if no schema or windows path like C:\a.txt, return FILE
|
|
||||||
if index == -1 || index == 1 {
|
if index == -1 || index == 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
Loaded 50 of 328 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user