mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 04:13:43 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f778596390 | ||
|
|
c2cbc14492 | ||
|
|
62c22bc6b4 | ||
|
|
b89c7bc608 | ||
|
|
f2cd686563 | ||
|
|
ef5ee99f04 | ||
|
|
6d136604db | ||
|
|
38ff711ea9 | ||
|
|
a1a8abc25f | ||
|
|
abccbabc78 | ||
|
|
c26eb26b8f | ||
|
|
74beea6044 | ||
|
|
e548d7e0c9 | ||
|
|
a996d57914 | ||
|
|
f7dc60ad50 | ||
|
|
8425a96ac9 | ||
|
|
99f4e8c895 | ||
|
|
cc0f61b527 | ||
|
|
922ef969a6 | ||
|
|
3a542054fc | ||
|
|
895a8d2ee4 | ||
|
|
29d0f5ebcf | ||
|
|
31228e3edf | ||
|
|
31a32d7e14 | ||
|
|
65fd6fb489 | ||
|
|
953b878a89 | ||
|
|
ae4ca3da58 | ||
|
|
d09bd180af | ||
|
|
3d24e353c5 | ||
|
|
ce7489b0c0 | ||
|
|
2b5091ba21 | ||
|
|
ff32971aca | ||
|
|
f2af61f9aa | ||
|
|
d935b9d631 | ||
|
|
ae66d1d83d | ||
|
|
aa91ca1d50 | ||
|
|
2d4bdea5cb | ||
|
|
2f2393ac20 | ||
|
|
5a761e244a | ||
|
|
6538766780 | ||
|
|
16b7a3d5b3 | ||
|
|
ef97528114 | ||
|
|
b58669bb46 | ||
|
|
51d28c5e53 | ||
|
|
db15650664 | ||
|
|
ee7da179c1 | ||
|
|
6bb32372ab | ||
|
|
7d6428c967 | ||
|
|
bba5107dd5 | ||
|
|
ddfe76b635 | ||
|
|
aad7395dde | ||
|
|
6f541715df | ||
|
|
848797b2fd | ||
|
|
0a05bf41e7 | ||
|
|
485656d1d4 | ||
|
|
c074e0c2bc | ||
|
|
0db0a2c987 | ||
|
|
b06a79b5c7 | ||
|
|
4840c4b8a4 | ||
|
|
e72b097aa2 | ||
|
|
af52c4ce7e | ||
|
|
aae79c2449 | ||
|
|
83838afce9 | ||
|
|
4690107e32 | ||
|
|
74b08b3631 | ||
|
|
953d348c0c | ||
|
|
00cea68b6e | ||
|
|
daa9d4c707 | ||
|
|
6bc587752a | ||
|
|
11befc09bd | ||
|
|
ff6c0da262 | ||
|
|
aab4a37a7e | ||
|
|
74c9491893 | ||
|
|
256faf2cbd | ||
|
|
32e3620f5e |
No files matched your search
@@ -19,6 +19,8 @@ categories:
|
|||||||
label: 'doc'
|
label: 'doc'
|
||||||
- title: '🚀 CI/CD'
|
- title: '🚀 CI/CD'
|
||||||
label: 'ci'
|
label: 'ci'
|
||||||
|
- title: '🌎 Internationalization'
|
||||||
|
label: 'translation'
|
||||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||||
version-resolver:
|
version-resolver:
|
||||||
major:
|
major:
|
||||||
@@ -32,6 +34,10 @@ version-resolver:
|
|||||||
- 'patch'
|
- 'patch'
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
## Changes
|
# Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
# 更新日志
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
+149
-73
@@ -2,10 +2,19 @@ name: build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
platform:
|
||||||
|
description: 'Build platform'
|
||||||
|
required: true
|
||||||
|
default: 'all'
|
||||||
|
test:
|
||||||
|
description: 'Test mode'
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.21"
|
||||||
FLUTTER_VERSION: "3.10.6"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -14,39 +23,41 @@ jobs:
|
|||||||
get-release:
|
get-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
tag_name: ${{ steps.get-release.outputs.tag_name }}
|
version: ${{ steps.get-release.outputs.version }}
|
||||||
upload_url: ${{ steps.get-release.outputs.upload_url }}
|
upload_url: ${{ steps.get-release.outputs.upload_url }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: monkeyWie/get-latest-release@v1
|
- uses: monkeyWie/get-latest-release@v2.1
|
||||||
id: get-release
|
id: get-release
|
||||||
with:
|
with:
|
||||||
myToken: ${{ github.token }}
|
myToken: ${{ github.token }}
|
||||||
|
|
||||||
build-windows:
|
build-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
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
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" -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=$VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
|
$TAG = "v$env:VERSION"
|
||||||
flutter build windows
|
flutter build windows
|
||||||
$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\runner\Release\"
|
$release = "build\windows\x64\runner\Release\"
|
||||||
cp $mingw\libstdc++-6.dll $release
|
cp $mingw\libstdc++-6.dll $release
|
||||||
cp $mingw\libgcc_s_seh-1.dll $release
|
cp $mingw\libgcc_s_seh-1.dll $release
|
||||||
cp $mingw\libwinpthread-1.dll $release
|
cp $mingw\libwinpthread-1.dll $release
|
||||||
@@ -55,7 +66,7 @@ jobs:
|
|||||||
cp $system\vcruntime140_1.dll $release
|
cp $system\vcruntime140_1.dll $release
|
||||||
|
|
||||||
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-$env:VERSION-windows-amd64-portable.zip"
|
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
|
||||||
|
|
||||||
cd build/windows
|
cd build/windows
|
||||||
echo @"
|
echo @"
|
||||||
@@ -63,7 +74,7 @@ jobs:
|
|||||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||||
|
|
||||||
#define MyAppName "Gopeed"
|
#define MyAppName "Gopeed"
|
||||||
#define MyAppVersion "$($env:VERSION.substring(1))"
|
#define MyAppVersion "$env:VERSION"
|
||||||
#define MyAppPublisher "monkeyWie"
|
#define MyAppPublisher "monkeyWie"
|
||||||
#define MyAppURL "https://gopeed.com"
|
#define MyAppURL "https://gopeed.com"
|
||||||
#define MyAppExeName "gopeed.exe"
|
#define MyAppExeName "gopeed.exe"
|
||||||
@@ -79,7 +90,7 @@ jobs:
|
|||||||
AppPublisherURL={#MyAppURL}
|
AppPublisherURL={#MyAppURL}
|
||||||
AppSupportURL={#MyAppURL}
|
AppSupportURL={#MyAppURL}
|
||||||
AppUpdatesURL={#MyAppURL}
|
AppUpdatesURL={#MyAppURL}
|
||||||
DefaultDirName={autopf}\gopeed
|
DefaultDirName={autopf}\gopeedrsion
|
||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
LicenseFile=..\..\..\..\LICENSE
|
LicenseFile=..\..\..\..\LICENSE
|
||||||
; Remove the following line to run in administrative install mode (install for all users.)
|
; Remove the following line to run in administrative install mode (install for all users.)
|
||||||
@@ -100,7 +111,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: ".\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]
|
||||||
@@ -114,9 +125,9 @@ jobs:
|
|||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
"@ > setup.iss
|
"@ > setup.iss
|
||||||
iscc.exe setup.iss
|
iscc.exe setup.iss
|
||||||
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
mv "Output\gopeed.exe" "Output\Gopeed-$TAG-windows-amd64.exe"
|
||||||
Compress-Archive -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe" -DestinationPath "Output\Gopeed-$env:VERSION-windows-amd64.zip"
|
Compress-Archive -Path "Output\Gopeed-$TAG-windows-amd64.exe" -DestinationPath "Output\Gopeed-$TAG-windows-amd64.zip"
|
||||||
Remove-Item -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
Remove-Item -Path "Output\Gopeed-$TAG-windows-amd64.exe"
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -124,6 +135,7 @@ jobs:
|
|||||||
asset_path: ui/flutter/build/windows/Output/*
|
asset_path: ui/flutter/build/windows/Output/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-macos-arm64-lib:
|
build-macos-arm64-lib:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -132,15 +144,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go install src.techknowlogick.com/xgo@latest
|
go install src.techknowlogick.com/xgo@latest
|
||||||
xgo -go go-1.19.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
||||||
mv libgopeed-*.dylib libgopeed.dylib
|
mv libgopeed-*.dylib libgopeed.dylib
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-arm64-lib
|
||||||
path: libgopeed.dylib
|
path: libgopeed.dylib
|
||||||
build-macos:
|
build-macos:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
needs: [ get-release, build-macos-arm64-lib ]
|
needs: [ get-release, build-macos-arm64-lib ]
|
||||||
steps:
|
steps:
|
||||||
@@ -155,19 +170,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Install appdmg
|
- name: Install appdmg
|
||||||
run: npm install -g appdmg
|
run: |
|
||||||
|
python3 -m pip install setuptools
|
||||||
|
npm install -g appdmg
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-arm64-lib
|
||||||
path: ui/flutter/lib-arm64
|
path: ui/flutter/lib-arm64
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
PROJECT_DIR=$(pwd)
|
PROJECT_DIR=$(pwd)
|
||||||
|
|
||||||
# amd64 lib
|
# amd64 lib
|
||||||
go build -tags nosqlite -ldflags="-w -s" -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
|
||||||
# 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
|
||||||
@@ -189,7 +206,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
mkdir dist
|
mkdir dist
|
||||||
appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
|
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -197,7 +214,8 @@ jobs:
|
|||||||
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
|
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-linux:
|
build-linux:
|
||||||
runs-on: ubuntu-latest
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -208,55 +226,108 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
sudo apt update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
# flutter build dependencies
|
||||||
|
sudo apt install -y ninja-build libgtk-3-dev libappindicator3-dev
|
||||||
|
# rpm build dependencies
|
||||||
|
sudo apt install -y rpm patchelf
|
||||||
|
# appimage build dependencies
|
||||||
|
sudo apt install -y libfuse2 locate
|
||||||
|
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||||
|
chmod +x appimagetool
|
||||||
|
sudo mv appimagetool /usr/local/bin/
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
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
|
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
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build linux
|
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||||
mkdir -p debian/gui
|
flutter_distributor package --platform linux --targets appimage,deb
|
||||||
cp assets/icon/icon_1024.png debian/gui/gopeed.png
|
cd dist/*
|
||||||
cat>debian/debian.yaml<<EOF
|
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
|
||||||
flutter_app:
|
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
|
||||||
command: gopeed
|
|
||||||
arch: x64
|
|
||||||
parent: /usr/local/lib
|
|
||||||
control:
|
|
||||||
Package: gopeed
|
|
||||||
Version: ${VERSION:1}
|
|
||||||
Architecture: amd64
|
|
||||||
Essential: no
|
|
||||||
Priority: optional
|
|
||||||
Depends:
|
|
||||||
Maintainer: monkeyWie
|
|
||||||
Description: High speed downloader that supports all platforms.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat>debian/gui/gopeed.desktop<<EOF
|
|
||||||
[Desktop Entry]
|
|
||||||
Version=${VERSION:1}
|
|
||||||
Name=Gopeed
|
|
||||||
GenericName=Gopeed
|
|
||||||
Comment=High speed downloader that supports all platforms.
|
|
||||||
Terminal=false
|
|
||||||
Type=Application
|
|
||||||
Categories=Utility
|
|
||||||
Keywords=Flutter;Downloader;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
dart pub global activate flutter_to_debian
|
|
||||||
flutter_to_debian
|
|
||||||
mv debian/packages/gopeed*.deb debian/packages/Gopeed-$VERSION-linux-amd64.deb
|
|
||||||
- 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: ui/flutter/debian/packages/*
|
asset_path: ui/flutter/dist/*/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
|
build-snap:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ get-release ]
|
||||||
|
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: Setup LXD
|
||||||
|
uses: canonical/[email protected]
|
||||||
|
with:
|
||||||
|
channel: latest/stable
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
|
||||||
|
sudo snap install snapcraft --classic
|
||||||
|
mkdir -p snap/gui
|
||||||
|
cp assets/icon/icon.svg snap/gui/gopeed.svg
|
||||||
|
|
||||||
|
cat>snap/snapcraft.yaml<<EOF
|
||||||
|
name: gopeed
|
||||||
|
version: $VERSION
|
||||||
|
summary: A modern download manager
|
||||||
|
description: High speed downloader that supports all platforms.
|
||||||
|
|
||||||
|
confinement: strict
|
||||||
|
base: core22
|
||||||
|
grade: stable
|
||||||
|
|
||||||
|
architectures:
|
||||||
|
- build-on: amd64
|
||||||
|
build-for: amd64
|
||||||
|
|
||||||
|
slots:
|
||||||
|
dbus-gopeed:
|
||||||
|
interface: dbus
|
||||||
|
bus: session
|
||||||
|
name: com.gopeed.gopeed
|
||||||
|
|
||||||
|
apps:
|
||||||
|
gopeed:
|
||||||
|
command: gopeed
|
||||||
|
extensions: [gnome]
|
||||||
|
plugs:
|
||||||
|
- network
|
||||||
|
slots:
|
||||||
|
- dbus-gopeed
|
||||||
|
parts:
|
||||||
|
gopeed:
|
||||||
|
source: .
|
||||||
|
plugin: flutter
|
||||||
|
stage-packages:
|
||||||
|
- libgtk-3-dev
|
||||||
|
- libappindicator3-dev
|
||||||
|
flutter-target: lib/main.dart
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
||||||
|
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
|
||||||
|
|
||||||
|
snapcraft --use-lxd
|
||||||
|
|
||||||
|
# Snapcraft login
|
||||||
|
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }}
|
||||||
|
snapcraft upload --release=${{ github.event.inputs.test == 'true' && 'edge' || 'stable' }} gopeed_${VERSION}_amd64.snap
|
||||||
build-android:
|
build-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:
|
||||||
@@ -273,7 +344,7 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
APK_KEYSTORE: ${{ secrets.APK_KEYSTORE }}
|
APK_KEYSTORE: ${{ secrets.APK_KEYSTORE }}
|
||||||
APK_KEY_PASSWORD: ${{ secrets.APK_KEY_PASSWORD }}
|
APK_KEY_PASSWORD: ${{ secrets.APK_KEY_PASSWORD }}
|
||||||
APK_STORE_PASSWORD: ${{ secrets.APK_STORE_PASSWORD }}
|
APK_STORE_PASSWORD: ${{ secrets.APK_STORE_PASSWORD }}
|
||||||
@@ -281,12 +352,12 @@ 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" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed 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/android/app/libs/libgopeed.aar -target=android -androidapi 19 -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
|
||||||
mkdir dist
|
mkdir dist
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk
|
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -294,6 +365,7 @@ jobs:
|
|||||||
asset_path: ui/flutter/dist/*
|
asset_path: ui/flutter/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-ios:
|
build-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:
|
||||||
@@ -306,19 +378,19 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
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" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite,netgo -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
|
||||||
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
|
||||||
mkdir dist
|
mkdir dist
|
||||||
mv Payload.zip dist/Gopeed-$VERSION-ios.ipa
|
mv Payload.zip dist/Gopeed-v$VERSION-ios.ipa
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -326,6 +398,7 @@ 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' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -338,13 +411,14 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --web-renderer html
|
flutter build web --web-renderer html
|
||||||
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
|
||||||
@@ -354,9 +428,9 @@ jobs:
|
|||||||
if [ $goos = "darwin" ]; then
|
if [ $goos = "darwin" ]; then
|
||||||
goos_name="macos"
|
goos_name="macos"
|
||||||
fi
|
fi
|
||||||
name=gopeed-web-$VERSION-$goos_name-$goarch
|
name=gopeed-web-v$VERSION-$goos_name-$goarch
|
||||||
dir="dist/$name/"
|
dir="dist/$name/"
|
||||||
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w" -o $dir github.com/GopeedLab/gopeed/cmd/web \
|
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o $dir github.com/GopeedLab/gopeed/cmd/web \
|
||||||
&& cd $dir \
|
&& cd $dir \
|
||||||
&& file=$(ls -AU | head -1) \
|
&& file=$(ls -AU | head -1) \
|
||||||
&& mkdir $name \
|
&& mkdir $name \
|
||||||
@@ -373,6 +447,7 @@ jobs:
|
|||||||
asset_path: dist/zip/*
|
asset_path: dist/zip/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-docker:
|
build-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:
|
||||||
@@ -393,15 +468,16 @@ jobs:
|
|||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
with:
|
with:
|
||||||
push: true
|
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
||||||
|
build-args: VERSION=${{ env.VERSION }}
|
||||||
platforms: |
|
platforms: |
|
||||||
linux/386
|
linux/386
|
||||||
linux/amd64
|
linux/amd64
|
||||||
linux/arm64
|
linux/arm64
|
||||||
linux/arm/v6
|
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
|
linux/arm64/v8
|
||||||
tags: |
|
tags: |
|
||||||
liwei2633/gopeed:latest
|
liwei2633/gopeed:latest
|
||||||
liwei2633/gopeed:${{ env.VERSION }}
|
liwei2633/gopeed:v${{ env.VERSION }}
|
||||||
@@ -27,8 +27,8 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.21"
|
||||||
FLUTTER_VERSION: "3.10.6"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
ui/flutter/dist/
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
|||||||
+1
-22
@@ -17,17 +17,9 @@ start the front-end flutter project in `debug` mode to run.
|
|||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
The internationalization files of Gopeed are located in the `ui/flutter/assets/locales` directory.
|
The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n/langs` directory.
|
||||||
You only need to add the corresponding language file in this directory.
|
You only need to add the corresponding language file in this directory.
|
||||||
|
|
||||||
|
|
||||||
Generate locales after you edit locales:
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
get generate locales
|
|
||||||
```
|
|
||||||
|
|
||||||
Please refer to `en_us.dart` for translation.
|
Please refer to `en_us.dart` for translation.
|
||||||
|
|
||||||
## flutter development
|
## flutter development
|
||||||
@@ -39,16 +31,3 @@ Turn on build_runner watcher if you want to edit api/models:
|
|||||||
```
|
```
|
||||||
flutter pub run build_runner watch
|
flutter pub run build_runner watch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
get-cli commands usages:
|
|
||||||
|
|
||||||
```
|
|
||||||
create:
|
|
||||||
controller: Generate controller
|
|
||||||
page: Use to generate pages
|
|
||||||
view: Generate view
|
|
||||||
generate:
|
|
||||||
locales: Generate translation file from json files
|
|
||||||
model: generate Class model from json
|
|
||||||
```
|
|
||||||
+1
-22
@@ -17,17 +17,9 @@
|
|||||||
|
|
||||||
## 翻訳
|
## 翻訳
|
||||||
|
|
||||||
Gopeed の国際化ファイルは `ui/flutter/assets/locales` ディレクトリにあります。
|
Gopeed の国際化ファイルは `ui/flutter/lib/i18n/langs` ディレクトリにあります。
|
||||||
このディレクトリに対応する言語ファイルを追加するだけでよいです。
|
このディレクトリに対応する言語ファイルを追加するだけでよいです。
|
||||||
|
|
||||||
|
|
||||||
ロケール編集後にロケールを生成:
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
get generate locales
|
|
||||||
```
|
|
||||||
|
|
||||||
翻訳については `en_us.dart` を参照してください。
|
翻訳については `en_us.dart` を参照してください。
|
||||||
|
|
||||||
## flutter での開発
|
## flutter での開発
|
||||||
@@ -39,16 +31,3 @@ api/models を編集したい場合は build_runner watcher をオンにしま
|
|||||||
```
|
```
|
||||||
flutter pub run build_runner watch
|
flutter pub run build_runner watch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
get-cli コマンドの使用法:
|
|
||||||
|
|
||||||
```
|
|
||||||
create:
|
|
||||||
controller: Generate controller
|
|
||||||
page: Use to generate pages
|
|
||||||
view: Generate view
|
|
||||||
generate:
|
|
||||||
locales: Generate translation file from json files
|
|
||||||
model: generate Class model from json
|
|
||||||
```
|
|
||||||
+2
-20
@@ -14,15 +14,9 @@ flutter 项目即可运行。
|
|||||||
|
|
||||||
## 翻译
|
## 翻译
|
||||||
|
|
||||||
Gopeed 的国际化文件位于 `ui/flutter/assets/locales` 目录下,只需要在该目录下添加对应的语言文件即可。
|
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
|
||||||
|
|
||||||
编辑locales后请运行以下命令:
|
请注意以 `en_us.dart` 为参照进行翻译。
|
||||||
|
|
||||||
```
|
|
||||||
get generate locales
|
|
||||||
```
|
|
||||||
|
|
||||||
请注意以 `en_US.json` 为参照进行翻译。
|
|
||||||
|
|
||||||
## flutter开发
|
## flutter开发
|
||||||
|
|
||||||
@@ -34,15 +28,3 @@ get generate locales
|
|||||||
flutter pub run build_runner watch
|
flutter pub run build_runner watch
|
||||||
```
|
```
|
||||||
|
|
||||||
适用get-cli 命令:
|
|
||||||
|
|
||||||
```
|
|
||||||
create:
|
|
||||||
controller: Generate controller
|
|
||||||
page: Use to generate pages
|
|
||||||
view: Generate view
|
|
||||||
generate:
|
|
||||||
locales: Generate translation file from json files
|
|
||||||
model: generate Class model from json
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Gopeed 協助指南
|
||||||
|
|
||||||
|
首先感謝您願意幫助我們改進並優化該項目,這份指南將會幫助您更好的參與 Gopeed 的開發。
|
||||||
|
|
||||||
|
## 分支說明
|
||||||
|
|
||||||
|
本項目只有一個分支,即 `main` 分支,如果您想要參與 Gopeed 的開發,請先 fork 該項目,再在您自己的 fork 中進行開發,開發完成後再開啟PR,以合併至 `main` 分支。
|
||||||
|
|
||||||
|
## 離線開發
|
||||||
|
|
||||||
|
建議使用 web 端進行開發與調試,首先啟動服務,使用指令 `go run cmd/api/main.go` 啟動 ,該服務默認連接埠為 `9999`,接著以 `debug` 模式啟動前端 flutter 項目即可。
|
||||||
|
|
||||||
|
## 翻譯
|
||||||
|
|
||||||
|
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
|
||||||
|
|
||||||
|
|
||||||
|
請以 `en_us.dart` 作為參照。
|
||||||
|
|
||||||
|
## flutter開發
|
||||||
|
|
||||||
|
每次提交PR前請務必執行 `dart format ./ui/flutter`
|
||||||
|
|
||||||
|
如果需要編輯 api/models,請打開build_runner watcher:
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub run build_runner watch
|
||||||
|
```
|
||||||
+5
-3
@@ -1,21 +1,23 @@
|
|||||||
FROM instrumentisto/flutter:3.10.6 AS flutter
|
FROM instrumentisto/flutter:3.16.8 AS flutter
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
||||||
RUN flutter pub get
|
RUN flutter pub get
|
||||||
COPY ./ui/flutter ./
|
COPY ./ui/flutter ./
|
||||||
RUN flutter build web --web-renderer html
|
RUN flutter build web --web-renderer html
|
||||||
|
|
||||||
FROM golang:1.19.3 AS go
|
FROM golang:1.21.6 AS go
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./go.mod ./go.sum ./
|
COPY ./go.mod ./go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=flutter /app/build/web ./cmd/web/dist
|
COPY --from=flutter /app/build/web ./cmd/web/dist
|
||||||
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=${VERSION}" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
|
||||||
FROM alpine:3.14.2
|
FROM alpine:3.14.2
|
||||||
LABEL maintainer="monkeyWie"
|
LABEL maintainer="monkeyWie"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=go /app/dist/gopeed ./
|
COPY --from=go /app/dist/gopeed ./
|
||||||
|
VOLUME ["/app/storage"]
|
||||||
EXPOSE 9999
|
EXPOSE 9999
|
||||||
ENTRYPOINT ["./gopeed"]
|
ENTRYPOINT ["./gopeed"]
|
||||||
@@ -9,29 +9,80 @@
|
|||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.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.
|
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) | 📖 [Develop Docs](https://docs.gopeed.com)
|
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
|
||||||
|
|
||||||
## Install
|
## Download
|
||||||
|
|
||||||
**Supported platforms**
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Platform</th>
|
||||||
|
<th>Package Type</th>
|
||||||
|
<th>Download Link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
- [x] windows
|
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||||
- [x] macos
|
|
||||||
- [x] linux
|
|
||||||
- [x] android
|
|
||||||
- [x] ios
|
|
||||||
- [x] web
|
|
||||||
- [x] docker
|
|
||||||
|
|
||||||
[Download](https://github.com/GopeedLab/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal
|
|
||||||
|
|
||||||
### Command tool
|
### Command tool
|
||||||
|
|
||||||
@@ -41,24 +92,6 @@ use `go install`:
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
#### Start directly
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Access Gopeed
|
|
||||||
|
|
||||||
When the docker container is running, you can access the web page through `http://localhost:9999`.
|
|
||||||
|
|
||||||
## 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!
|
||||||
@@ -75,8 +108,8 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.21+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|
||||||
@@ -159,7 +192,6 @@ 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
|
||||||
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
|
||||||
go run cmd/web/main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|||||||
+68
-44
@@ -9,59 +9,84 @@
|
|||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
||||||
|
|
||||||
## はじめに
|
## はじめに
|
||||||
|
|
||||||
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。
|
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>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Platform</th>
|
||||||
|
<th>Package Type</th>
|
||||||
|
<th>Download Link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
- [x] windows
|
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||||
- [x] macos
|
|
||||||
- [x] linux
|
|
||||||
- [x] android
|
|
||||||
- [x] ios
|
|
||||||
- [x] web
|
|
||||||
- [x] docker
|
|
||||||
|
|
||||||
[ダウンロード](https://github.com/GopeedLab/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> ヒント: macos を開くのに失敗した場合は、ターミナルで `xattr -d com.apple.quarantine /Applications/Gopeed.app` コマンドを実行してください
|
|
||||||
|
|
||||||
### コマンドツール
|
|
||||||
|
|
||||||
`go install` を使用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
#### 直接始める
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Docker Compose の使用
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Gopeed へのアクセス
|
|
||||||
|
|
||||||
docker コンテナが起動しているときは、`http://localhost:9999` からウェブページにアクセスできます。
|
|
||||||
|
|
||||||
## 寄付
|
## 寄付
|
||||||
|
|
||||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
|
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||||
|
|
||||||
## ショーケース
|
## ショーケース
|
||||||
|
|
||||||
@@ -75,8 +100,8 @@ docker コンテナが起動しているときは、`http://localhost:9999` か
|
|||||||
|
|
||||||
### 環境
|
### 環境
|
||||||
|
|
||||||
1. Go 言語 1.19+
|
1. Go 言語 1.21+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### クローン
|
### クローン
|
||||||
|
|
||||||
@@ -159,7 +184,6 @@ 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
|
||||||
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
|
||||||
go run cmd/web/main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感謝
|
## 感謝
|
||||||
|
|||||||
+69
-37
@@ -9,29 +9,80 @@
|
|||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
||||||
|
|
||||||
## 介绍
|
## 介绍
|
||||||
|
|
||||||
Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
|
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>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>运行平台</th>
|
||||||
|
<th>安装包类型</th>
|
||||||
|
<th>下载链接</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
- [x] windows
|
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||||
- [x] macos
|
|
||||||
- [x] linux
|
|
||||||
- [x] android
|
|
||||||
- [x] ios
|
|
||||||
- [x] web
|
|
||||||
- [x] docker
|
|
||||||
|
|
||||||
[前往下载](https://github.com/GopeedLab/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
|
|
||||||
|
|
||||||
### 命令行工具
|
### 命令行工具
|
||||||
|
|
||||||
@@ -41,24 +92,6 @@ Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
#### 直接运行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 使用 Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 访问服务
|
|
||||||
|
|
||||||
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
|
||||||
|
|
||||||
## 赞助
|
## 赞助
|
||||||
|
|
||||||
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
||||||
@@ -75,8 +108,8 @@ docker-compose up -d
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.21+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆项目
|
### 克隆项目
|
||||||
|
|
||||||
@@ -159,7 +192,6 @@ 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
|
||||||
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
|
||||||
go run cmd/web/main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感谢
|
## 感谢
|
||||||
|
|||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/donate.html)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
||||||
|
|
||||||
|
## 簡介
|
||||||
|
|
||||||
|
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
|
||||||
|
|
||||||
|
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
||||||
|
|
||||||
|
## 下載
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>運行平台</th>
|
||||||
|
<th>安裝包類型</th>
|
||||||
|
<th>下載鏈接</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
|
### 使用CLI安裝
|
||||||
|
|
||||||
|
使用`go install`安裝:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 贊助
|
||||||
|
|
||||||
|
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||||
|
|
||||||
|
## 軟體介面
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 開發
|
||||||
|
|
||||||
|
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
|
||||||
|
|
||||||
|
> 前端代碼位於`ui/flutter`目錄內。
|
||||||
|
|
||||||
|
### 開發環境
|
||||||
|
|
||||||
|
1. Golang 1.21+
|
||||||
|
2. Flutter 3.16+
|
||||||
|
|
||||||
|
### 克隆項目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 協助開發
|
||||||
|
|
||||||
|
請參考[協助指南](CONTRIBUTING_zh-TW.md)
|
||||||
|
|
||||||
|
### 編譯
|
||||||
|
|
||||||
|
#### 桌面端
|
||||||
|
|
||||||
|
首先需要按照[flutter desktop 官方文檔](https://docs.flutter.dev/development/platform-integration/desktop)配置開發環境,並準備好`cgo`環境,具體方法可以自行搜索。
|
||||||
|
|
||||||
|
組建指令:
|
||||||
|
|
||||||
|
- windows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
- macos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
- linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 移動設備
|
||||||
|
|
||||||
|
需要`cgo`環境,並安裝`gomobile`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
```
|
||||||
|
|
||||||
|
組建指令:
|
||||||
|
|
||||||
|
- android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build apk
|
||||||
|
```
|
||||||
|
|
||||||
|
- ios
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 ios --no-codesign
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 網頁端
|
||||||
|
|
||||||
|
組建指令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 感謝
|
||||||
|
|
||||||
|
### 貢獻者
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
|
## 軟體許可
|
||||||
|
|
||||||
|
該軟體遵循 [GPLv3](LICENSE) 。
|
||||||
@@ -11,6 +11,7 @@ func main() {
|
|||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
Address: "127.0.0.1:9999",
|
Address: "127.0.0.1:9999",
|
||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
|
WebEnable: true,
|
||||||
}
|
}
|
||||||
cmd.Start(cfg)
|
cmd.Start(cfg)
|
||||||
}
|
}
|
||||||
+14
-1
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
@@ -47,16 +48,28 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
printProgress(emptyTask, "downloading...")
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lastLineLen = 0
|
lastLineLen = 0
|
||||||
sb = new(strings.Builder)
|
sb = new(strings.Builder)
|
||||||
|
emptyTask = &download.Task{
|
||||||
|
Progress: &download.Progress{},
|
||||||
|
Meta: &fetcher.FetcherMeta{
|
||||||
|
Res: &base.Resource{},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func printProgress(task *download.Task, title string) {
|
func printProgress(task *download.Task, title string) {
|
||||||
rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
|
var rate float64
|
||||||
|
if task.Meta.Res.Size <= 0 {
|
||||||
|
rate = 0
|
||||||
|
} else {
|
||||||
|
rate = float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
|
||||||
|
}
|
||||||
completeWidth := int(progressWidth * rate)
|
completeWidth := int(progressWidth * rate)
|
||||||
speed := util.ByteFmt(task.Progress.Speed)
|
speed := util.ByteFmt(task.Progress.Speed)
|
||||||
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ func parse() *args {
|
|||||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Password")
|
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
||||||
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
|
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
|
||||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/cmd"
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
@@ -29,10 +31,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
|
||||||
cfg := &model.StartConfig{
|
cfg := &model.StartConfig{
|
||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
|
StorageDir: filepath.Join(dir, "storage"),
|
||||||
ApiToken: *args.ApiToken,
|
ApiToken: *args.ApiToken,
|
||||||
ProductionMode: true,
|
ProductionMode: true,
|
||||||
WebEnable: true,
|
WebEnable: true,
|
||||||
|
|||||||
+3
-2
@@ -4,8 +4,9 @@ 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
|
image: liwei2633/gopeed
|
||||||
volumes:
|
volumes:
|
||||||
- ~/Downloads:/root/Downloads # replace with your prefer path
|
- ~/gopeed/Downloads:/root/Downloads # mount download path
|
||||||
|
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -1,107 +1,117 @@
|
|||||||
module github.com/GopeedLab/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.19
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
|
github.com/anacrolix/missinggo/v2 v2.7.3
|
||||||
github.com/anacrolix/torrent v1.52.5
|
github.com/anacrolix/torrent v1.53.3
|
||||||
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562
|
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||||
|
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
||||||
github.com/go-git/go-git/v5 v5.8.1
|
github.com/go-git/go-git/v5 v5.8.1
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||||
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6
|
go.etcd.io/bbolt v1.3.8
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||||
|
golang.org/x/sync v0.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
|
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.2.3 // indirect
|
github.com/RoaringBitmap/roaring v1.7.0 // indirect
|
||||||
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.3.0 // indirect
|
github.com/anacrolix/chansync v0.4.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
|
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.2.1 // indirect
|
github.com/anacrolix/envpprof v1.3.0 // indirect
|
||||||
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // indirect
|
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
|
||||||
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||||
github.com/anacrolix/log v0.14.0 // indirect
|
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 // 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.0.0 // indirect
|
||||||
github.com/anacrolix/multiless v0.3.0 // indirect
|
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
||||||
github.com/anacrolix/stm v0.4.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.4.0 // indirect
|
github.com/anacrolix/sync v0.5.1 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
github.com/anacrolix/upnp v0.1.3 // indirect
|
||||||
github.com/anacrolix/utp v0.1.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.3.0 // indirect
|
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.2.2 // indirect
|
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // 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.7.0 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // 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.1.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-logr/logr v1.2.3 // indirect
|
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||||
|
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.1 // 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.3+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.3+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.2 // indirect
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.1 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.4.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/lispad/go-generics-tools v1.1.0 // 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.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.2 // indirect
|
github.com/pion/datachannel v1.5.5 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.4 // indirect
|
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||||
github.com/pion/ice/v2 v2.2.6 // indirect
|
github.com/pion/ice/v2 v2.3.12 // indirect
|
||||||
github.com/pion/interceptor v0.1.11 // indirect
|
github.com/pion/interceptor v0.1.25 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.5 // indirect
|
github.com/pion/mdns v0.0.9 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.9 // indirect
|
github.com/pion/rtcp v1.2.13 // indirect
|
||||||
github.com/pion/rtp v1.7.13 // indirect
|
github.com/pion/rtp v1.8.3 // indirect
|
||||||
github.com/pion/sctp v1.8.2 // indirect
|
github.com/pion/sctp v1.8.9 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.5 // indirect
|
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.9 // indirect
|
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||||
github.com/pion/stun v0.3.5 // indirect
|
github.com/pion/stun v0.6.1 // indirect
|
||||||
github.com/pion/transport v0.13.1 // indirect
|
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||||
github.com/pion/transport/v2 v2.0.0 // indirect
|
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
||||||
github.com/pion/udp v0.1.4 // indirect
|
|
||||||
github.com/pion/webrtc/v3 v3.1.42 // indirect
|
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rs/zerolog v1.31.0 // indirect
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
github.com/tidwall/btree v1.6.0 // indirect
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
github.com/tidwall/btree v1.7.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
go.opentelemetry.io/otel v1.8.0 // indirect
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.8.0 // indirect
|
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
|
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/crypto v0.18.0 // indirect
|
||||||
golang.org/x/net v0.12.0 // indirect
|
golang.org/x/mod v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/net v0.20.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
golang.org/x/tools v0.17.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
|
||||||
|
modernc.org/libc v1.40.6 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/sqlite v1.28.0 // indirect
|
||||||
|
zombiezen.com/go/sqlite v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
GetConfig func(v any) (bool, error)
|
GetConfig func(v any) bool
|
||||||
|
ProxyUrl *url.URL
|
||||||
FileController
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
}
|
}
|
||||||
@@ -21,8 +23,8 @@ type DefaultFileController struct {
|
|||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
GetConfig: func(v any) (bool, error) {
|
GetConfig: func(v any) bool {
|
||||||
return false, nil
|
return false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ type Fetcher interface {
|
|||||||
Pause() error
|
Pause() error
|
||||||
Close() error
|
Close() error
|
||||||
|
|
||||||
|
// Stats refreshes health statistics and returns the latest information
|
||||||
|
Stats() any
|
||||||
// Meta returns the meta information of the download.
|
// Meta returns the meta information of the download.
|
||||||
Meta() *FetcherMeta
|
Meta() *FetcherMeta
|
||||||
// Progress returns the progress of the download.
|
// Progress returns the progress of the download.
|
||||||
@@ -57,6 +59,15 @@ func (m *FetcherMeta) SingleFilepath() string {
|
|||||||
return path.Join(m.Opts.Path, file.Path, fileName)
|
return path.Join(m.Opts.Path, file.Path, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RootDirPath return the root dir path of the task file.
|
||||||
|
func (m *FetcherMeta) RootDirPath() string {
|
||||||
|
if m.Res.Name != "" {
|
||||||
|
return m.FolderPath()
|
||||||
|
} else {
|
||||||
|
return m.Opts.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FetcherBuilder defines the interface for a fetcher builder.
|
// FetcherBuilder defines the interface for a fetcher builder.
|
||||||
type FetcherBuilder interface {
|
type FetcherBuilder interface {
|
||||||
// Schemes returns the schemes supported by the fetcher.
|
// Schemes returns the schemes supported by the fetcher.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
ListenPort int `json:"listenPort"`
|
||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package bt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
@@ -9,7 +10,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"
|
||||||
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,6 +27,7 @@ var (
|
|||||||
|
|
||||||
type Fetcher struct {
|
type Fetcher struct {
|
||||||
ctl *controller.Controller
|
ctl *controller.Controller
|
||||||
|
config *config
|
||||||
|
|
||||||
torrent *torrent.Torrent
|
torrent *torrent.Torrent
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
@@ -42,6 +46,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
|||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
|
exist := f.ctl.GetConfig(&f.config)
|
||||||
|
if !exist {
|
||||||
|
f.config = &config{
|
||||||
|
ListenPort: 0,
|
||||||
|
Trackers: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +65,12 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := torrent.NewDefaultClientConfig()
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
cfg.ListenPort = 0
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||||
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||||
|
cfg.ListenPort = f.config.ListenPort
|
||||||
|
if f.ctl.ProxyUrl != nil {
|
||||||
|
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||||
|
}
|
||||||
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) {
|
||||||
@@ -87,7 +103,6 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
|
|||||||
if f.meta.Res != nil {
|
if f.meta.Res != nil {
|
||||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
||||||
}
|
}
|
||||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +116,14 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
ft.setTorrentDir(f.meta.FolderPath())
|
ft.setTorrentDir(f.meta.FolderPath())
|
||||||
}
|
}
|
||||||
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 len(f.meta.Opts.SelectFiles) == 0 {
|
||||||
|
f.meta.Opts.SelectFiles = make([]int, len(files))
|
||||||
|
for i := range files {
|
||||||
|
f.meta.Opts.SelectFiles[i] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
if len(f.meta.Opts.SelectFiles) == len(files) {
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||||
f.torrent.DownloadAll()
|
f.torrent.DownloadAll()
|
||||||
} else {
|
} else {
|
||||||
@@ -109,6 +132,7 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
file.Download()
|
file.Download()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +158,7 @@ func (f *Fetcher) safeDrop() {
|
|||||||
|
|
||||||
func (f *Fetcher) Wait() (err error) {
|
func (f *Fetcher) Wait() (err error) {
|
||||||
for {
|
for {
|
||||||
if f.torrentReady.Load() {
|
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
||||||
done := true
|
done := true
|
||||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
@@ -154,7 +178,7 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !selected {
|
if !selected {
|
||||||
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path()))
|
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -169,6 +193,16 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
return f.meta
|
return f.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Stats() any {
|
||||||
|
stats := f.torrent.Stats()
|
||||||
|
baseStats := &bt.Stats{
|
||||||
|
TotalPeers: stats.TotalPeers,
|
||||||
|
ActivePeers: stats.ActivePeers,
|
||||||
|
ConnectedSeeders: stats.ConnectedSeeders,
|
||||||
|
}
|
||||||
|
return baseStats
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
if !f.torrentReady.Load() {
|
if !f.torrentReady.Load() {
|
||||||
return f.progress
|
return f.progress
|
||||||
@@ -188,6 +222,7 @@ func (f *Fetcher) updateRes() {
|
|||||||
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()
|
||||||
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()),
|
||||||
@@ -195,7 +230,7 @@ func (f *Fetcher) updateRes() {
|
|||||||
Size: file.Length(),
|
Size: file.Length(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.CalcSize()
|
res.CalcSize(nil)
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
if f.meta.Opts != nil {
|
if f.meta.Opts != nil {
|
||||||
f.meta.Opts.InitSelectFiles(len(res.Files))
|
f.meta.Opts.InitSelectFiles(len(res.Files))
|
||||||
@@ -224,11 +259,6 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var cfg config
|
|
||||||
exist, err := f.ctl.GetConfig(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// use map to deduplicate
|
// use map to deduplicate
|
||||||
trackers := make(map[string]bool)
|
trackers := make(map[string]bool)
|
||||||
@@ -240,8 +270,8 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if exist && len(cfg.Trackers) > 0 {
|
if len(f.config.Trackers) > 0 {
|
||||||
for _, tracker := range cfg.Trackers {
|
for _, tracker := range f.config.Trackers {
|
||||||
trackers[tracker] = true
|
trackers[tracker] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,3 +311,12 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
||||||
|
func parseBep20() string {
|
||||||
|
s := strings.ReplaceAll(base.Version, ".", "")
|
||||||
|
if len(s) < 4 {
|
||||||
|
s += strings.Repeat("0", 4-len(s))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ 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"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -50,7 +52,15 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Config(t *testing.T) {
|
func TestFetcher_Config(t *testing.T) {
|
||||||
doResolve(t, buildConfigFetcher())
|
doResolve(t, buildConfigFetcher(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveWithProxy(t *testing.T) {
|
||||||
|
usr, pwd := "admin", "123"
|
||||||
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
|
defer proxyListener.Close()
|
||||||
|
|
||||||
|
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
@@ -90,7 +100,7 @@ func buildFetcher() fetcher.Fetcher {
|
|||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher() fetcher.Fetcher {
|
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherBuilder).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
mockCfg := config{
|
mockCfg := config{
|
||||||
@@ -98,12 +108,13 @@ func buildConfigFetcher() fetcher.Fetcher {
|
|||||||
"udp://tracker.birkenwald.de:6969/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
}}
|
}}
|
||||||
newController.GetConfig = func(v any) (bool, error) {
|
newController.GetConfig = func(v any) bool {
|
||||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||||
return false, err
|
return false
|
||||||
}
|
}
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
|
newController.ProxyUrl = proxyUrl
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
Connections int `json:"connections"`
|
Connections int `json:"connections"`
|
||||||
|
UseServerCtime bool `json:"useServerCtime"`
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/xiaoqidun/setft"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +53,7 @@ func newChunk(begin int64, end int64) *chunk {
|
|||||||
|
|
||||||
type Fetcher struct {
|
type Fetcher struct {
|
||||||
ctl *controller.Controller
|
ctl *controller.Controller
|
||||||
|
config *config
|
||||||
doneCh chan error
|
doneCh chan error
|
||||||
|
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
@@ -71,6 +74,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
|||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
|
exist := f.ctl.GetConfig(&f.config)
|
||||||
|
if !exist {
|
||||||
|
f.config = &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",
|
||||||
|
Connections: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +88,11 @@ 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
|
||||||
}
|
}
|
||||||
httpReq, err := buildRequest(nil, req)
|
httpReq, err := f.buildRequest(nil, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
client := buildClient()
|
client := f.buildClient()
|
||||||
// send Range request to check whether the server supports breakpoint continuation
|
// send Range request to check whether the server supports breakpoint continuation
|
||||||
// just test one byte, Range: bytes=0-0
|
// just test one byte, Range: bytes=0-0
|
||||||
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
||||||
@@ -96,8 +106,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
Range: false,
|
Range: false,
|
||||||
Files: []*base.FileInfo{},
|
Files: []*base.FileInfo{},
|
||||||
}
|
}
|
||||||
if base.HttpCodePartialContent == httpResp.StatusCode {
|
|
||||||
// 返回206响应码表示支持断点下载
|
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也表示支持断点下载
|
||||||
res.Range = true
|
res.Range = true
|
||||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||||
@@ -121,8 +132,17 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
} else {
|
} else {
|
||||||
return NewRequestError(httpResp.StatusCode, httpResp.Status)
|
return NewRequestError(httpResp.StatusCode, httpResp.Status)
|
||||||
}
|
}
|
||||||
|
// Parse last modified time
|
||||||
|
var lastModifiedTime *time.Time
|
||||||
|
lastModified := httpResp.Header.Get(base.HttpHeaderLastModified)
|
||||||
|
if lastModified != "" {
|
||||||
|
// ignore parse error
|
||||||
|
t, _ := time.Parse(time.RFC1123, lastModified)
|
||||||
|
lastModifiedTime = &t
|
||||||
|
}
|
||||||
file := &base.FileInfo{
|
file := &base.FileInfo{
|
||||||
Size: res.Size,
|
Size: res.Size,
|
||||||
|
Ctime: lastModifiedTime,
|
||||||
}
|
}
|
||||||
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
||||||
if contentDisposition != "" {
|
if contentDisposition != "" {
|
||||||
@@ -160,16 +180,7 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
|||||||
}
|
}
|
||||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||||
if extra.Connections == 0 {
|
if extra.Connections == 0 {
|
||||||
var cfg config
|
extra.Connections = f.config.Connections
|
||||||
exist, err := f.ctl.GetConfig(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exist {
|
|
||||||
extra.Connections = cfg.Connections
|
|
||||||
} else {
|
|
||||||
extra.Connections = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -218,6 +229,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
return f.meta
|
return f.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Stats() any {
|
||||||
|
// todo http download health
|
||||||
|
return &fhttp.Stats{}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
p := make(fetcher.Progress, 0)
|
p := make(fetcher.Progress, 0)
|
||||||
if len(f.chunks) > 0 {
|
if len(f.chunks) > 0 {
|
||||||
@@ -252,6 +268,10 @@ func (f *Fetcher) fetch() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.file.Close()
|
f.file.Close()
|
||||||
|
// Update file last modified time
|
||||||
|
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
||||||
|
setft.SetFileTime(f.file.Name(), time.Now(), *f.meta.Res.Files[0].Ctime, *f.meta.Res.Files[0].Ctime)
|
||||||
|
}
|
||||||
f.doneCh <- err
|
f.doneCh <- err
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -260,17 +280,17 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
chunk := f.chunks[index]
|
chunk := f.chunks[index]
|
||||||
chunk.retryTimes = 0
|
chunk.retryTimes = 0
|
||||||
|
|
||||||
httpReq, err := buildRequest(ctx, f.meta.Req)
|
httpReq, err := f.buildRequest(ctx, f.meta.Req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
client = buildClient()
|
client = f.buildClient()
|
||||||
buf = make([]byte, 8192)
|
buf = make([]byte, 8192)
|
||||||
maxRetries = 3
|
maxRetries = 3
|
||||||
)
|
)
|
||||||
// retry until all remain chunks failed
|
// retry until all remain chunks failed
|
||||||
for true {
|
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.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||||
return
|
return
|
||||||
@@ -307,22 +327,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
// retry request after 1 second
|
|
||||||
chunk.retryTimes = chunk.retryTimes + 1
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Http request success, reset retry times
|
// Http request success, reset retry times
|
||||||
chunk.retryTimes = 0
|
chunk.retryTimes = 0
|
||||||
err = func() error {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := resp.Body.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
@@ -339,7 +350,18 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}()
|
}()
|
||||||
|
if err != nil {
|
||||||
|
// If canceled, do not retry
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// retry request after 1 second
|
||||||
|
chunk.retryTimes = chunk.retryTimes + 1
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
// if chunk is completed, reset other not complete chunks retry times
|
// if chunk is completed, reset other not complete chunks retry times
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, c := range f.chunks {
|
for _, c := range f.chunks {
|
||||||
@@ -353,6 +375,52 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
||||||
|
url, err := url.Parse(req.URL)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
method string
|
||||||
|
body io.Reader
|
||||||
|
)
|
||||||
|
headers := make(map[string][]string)
|
||||||
|
if req.Extra == nil {
|
||||||
|
method = http.MethodGet
|
||||||
|
} else {
|
||||||
|
extra := req.Extra.(*fhttp.ReqExtra)
|
||||||
|
if extra.Method != "" {
|
||||||
|
method = extra.Method
|
||||||
|
} else {
|
||||||
|
method = http.MethodGet
|
||||||
|
}
|
||||||
|
if len(extra.Header) > 0 {
|
||||||
|
for k, v := range extra.Header {
|
||||||
|
headers[k] = []string{v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if extra.Body != "" {
|
||||||
|
body = bytes.NewBufferString(extra.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||||
|
// load user agent from config
|
||||||
|
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx != nil {
|
||||||
|
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
||||||
|
} else {
|
||||||
|
httpReq, err = http.NewRequest(method, url.String(), body)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpReq.Header = headers
|
||||||
|
return httpReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||||
if f.meta.Res.Range {
|
if f.meta.Res.Range {
|
||||||
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
||||||
@@ -381,59 +449,19 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildClient() *http.Client {
|
func (f *Fetcher) buildClient() *http.Client {
|
||||||
|
transport := &http.Transport{}
|
||||||
// Cookie handle
|
// Cookie handle
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
|
if f.ctl.ProxyUrl != nil {
|
||||||
|
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||||
|
}
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
|
Transport: transport,
|
||||||
Jar: jar,
|
Jar: jar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
|
||||||
url, err := url.Parse(req.URL)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
method string
|
|
||||||
body io.Reader
|
|
||||||
)
|
|
||||||
headers := make(map[string][]string)
|
|
||||||
if req.Extra == nil {
|
|
||||||
method = http.MethodGet
|
|
||||||
} else {
|
|
||||||
extra := req.Extra.(*fhttp.ReqExtra)
|
|
||||||
if extra.Method != "" {
|
|
||||||
method = extra.Method
|
|
||||||
} else {
|
|
||||||
method = http.MethodGet
|
|
||||||
}
|
|
||||||
if len(extra.Header) > 0 {
|
|
||||||
for k, v := range extra.Header {
|
|
||||||
headers[k] = []string{v}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if extra.Body != "" {
|
|
||||||
body = bytes.NewBufferString(extra.Body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
|
||||||
headers[base.HttpHeaderUserAgent] = []string{base.AgentName}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx != nil {
|
|
||||||
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
|
||||||
} else {
|
|
||||||
httpReq, err = http.NewRequest(method, url.String(), body)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
httpReq.Header = headers
|
|
||||||
return httpReq, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type fetcherData struct {
|
type fetcherData struct {
|
||||||
Chunks []*chunk
|
Chunks []*chunk
|
||||||
}
|
}
|
||||||
@@ -466,9 +494,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
fetcher.meta = meta
|
fetcher.meta = meta
|
||||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||||
if len(fd.Chunks) == 0 {
|
if len(fd.Chunks) > 0 {
|
||||||
fetcher.chunks = fetcher.splitChunk()
|
|
||||||
} else {
|
|
||||||
fetcher.chunks = fd.Chunks
|
fetcher.chunks = fd.Chunks
|
||||||
}
|
}
|
||||||
return fetcher
|
return fetcher
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ 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/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -68,7 +68,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.meta.Res) {
|
if !test.AssertResourceEqual(want, fetcher.meta.Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
|
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ func TestFetcher_DownloadError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadLimit(t *testing.T) {
|
func TestFetcher_DownloadLimit(t *testing.T) {
|
||||||
listener := test.StartTestLimitServer()
|
listener := test.StartTestLimitServer(4, 0)
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
@@ -141,10 +141,43 @@ func TestFetcher_DownloadResume(t *testing.T) {
|
|||||||
downloadResume(listener, 16, t)
|
downloadResume(listener, 16, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Config(t *testing.T) {
|
func TestFetcher_DownloadWithProxy(t *testing.T) {
|
||||||
|
httpListener := test.StartTestFileServer()
|
||||||
|
defer httpListener.Close()
|
||||||
|
proxyListener := test.StartSocks5Server("", "")
|
||||||
|
defer proxyListener.Close()
|
||||||
|
|
||||||
|
downloadWithProxy(httpListener, proxyListener, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ConfigConnections(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
fetcher := doDownloadReady(buildConfigFetcher(), listener, 0, t)
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
}), listener, 0, t)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fetcher.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := test.FileMd5(test.BuildFile)
|
||||||
|
got := test.FileMd5(test.DownloadFile)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Download() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
UseServerCtime: true,
|
||||||
|
}), listener, 0, t)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -302,23 +335,40 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||||
|
fetcher := downloadReady(httpListener, 4, t)
|
||||||
|
ctl := controller.NewController()
|
||||||
|
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
|
||||||
|
fetcher.Setup(ctl)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fetcher.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := test.FileMd5(test.BuildFile)
|
||||||
|
got := test.FileMd5(test.DownloadFile)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Download() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildFetcher() *Fetcher {
|
func buildFetcher() *Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherBuilder).Build()
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher.Setup(controller.NewController())
|
||||||
return fetcher.(*Fetcher)
|
return fetcher.(*Fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConfigFetcher() fetcher.Fetcher {
|
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherBuilder).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
mockCfg := config{
|
newController.GetConfig = func(v any) bool {
|
||||||
Connections: 16,
|
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
newController.GetConfig = func(v any) (bool, error) {
|
return true
|
||||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/armon/go-socks5"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -138,7 +141,7 @@ func StartTestErrorServer() net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StartTestLimitServer connections limit server
|
// StartTestLimitServer connections limit server
|
||||||
func StartTestLimitServer() 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() http.Handler {
|
||||||
@@ -148,7 +151,7 @@ func StartTestLimitServer() net.Listener {
|
|||||||
connections.Add(-1)
|
connections.Add(-1)
|
||||||
}()
|
}()
|
||||||
connections.Add(1)
|
connections.Add(1)
|
||||||
if connections.Load() > 4 {
|
if maxConnections != 0 && connections.Load() > maxConnections {
|
||||||
writer.WriteHeader(403)
|
writer.WriteHeader(403)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -162,7 +165,7 @@ func StartTestLimitServer() net.Listener {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
io.Copy(writer, file)
|
slowCopy(writer, file, delay)
|
||||||
} else {
|
} else {
|
||||||
// split range
|
// split range
|
||||||
s := strings.Split(r, "=")
|
s := strings.Split(r, "=")
|
||||||
@@ -203,13 +206,57 @@ func StartTestLimitServer() net.Listener {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
file.Seek(start, 0)
|
file.Seek(start, 0)
|
||||||
io.CopyN(writer, file, end-start+1)
|
slowCopyN(writer, file, end-start+1, delay)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
for {
|
||||||
|
nr, er := src.Read(buf)
|
||||||
|
if nr > 0 {
|
||||||
|
nw, ew := dst.Write(buf[0:nr])
|
||||||
|
if nw > 0 {
|
||||||
|
written += int64(nw)
|
||||||
|
}
|
||||||
|
if ew != nil {
|
||||||
|
err = ew
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if nr != nw {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if er != nil {
|
||||||
|
if er != io.EOF {
|
||||||
|
err = er
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if delay > 0 {
|
||||||
|
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||||
|
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
|
||||||
|
if written == n {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
if written < n && err == nil {
|
||||||
|
// src stopped early; must have been EOF.
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
func startTestServer(serverHandle func() 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 {
|
||||||
@@ -271,3 +318,32 @@ func ifExistAndRemove(name string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StartSocks5Server(usr, pwd string) net.Listener {
|
||||||
|
conf := &socks5.Config{}
|
||||||
|
if usr != "" && pwd != "" {
|
||||||
|
conf.Credentials = socks5.StaticCredentials{
|
||||||
|
usr: pwd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := socks5.New(conf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0") // 你可以根据需要更改监听地址
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
go server.Serve(listener)
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertResourceEqual(want, got *base.Resource) bool {
|
||||||
|
// Ignore ctime
|
||||||
|
if got != nil && len(got.Files) > 0 {
|
||||||
|
got.Files[0].Ctime = nil
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(want, got)
|
||||||
|
}
|
||||||
@@ -16,14 +16,13 @@ const (
|
|||||||
HttpCodePartialContent = 206
|
HttpCodePartialContent = 206
|
||||||
|
|
||||||
HttpHeaderRange = "Range"
|
HttpHeaderRange = "Range"
|
||||||
|
HttpHeaderAcceptRanges = "Accept-Ranges"
|
||||||
HttpHeaderContentLength = "Content-Length"
|
HttpHeaderContentLength = "Content-Length"
|
||||||
HttpHeaderContentRange = "Content-Range"
|
HttpHeaderContentRange = "Content-Range"
|
||||||
HttpHeaderContentDisposition = "Content-Disposition"
|
HttpHeaderContentDisposition = "Content-Disposition"
|
||||||
HttpHeaderUserAgent = "User-Agent"
|
HttpHeaderUserAgent = "User-Agent"
|
||||||
|
HttpHeaderLastModified = "Last-Modified"
|
||||||
|
|
||||||
|
HttpHeaderBytes = "bytes"
|
||||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
AgentName = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
|
||||||
)
|
|
||||||
+7
-2
@@ -3,6 +3,8 @@ package base
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request download request
|
// Request download request
|
||||||
@@ -47,11 +49,13 @@ func (r *Resource) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resource) CalcSize() {
|
func (r *Resource) CalcSize(selectFiles []int) {
|
||||||
var size int64
|
var size int64
|
||||||
for _, file := range r.Files {
|
for i, file := range r.Files {
|
||||||
|
if len(selectFiles) == 0 || slices.Contains(selectFiles, i) {
|
||||||
size += file.Size
|
size += file.Size
|
||||||
}
|
}
|
||||||
|
}
|
||||||
r.Size = size
|
r.Size = size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +63,7 @@ type FileInfo struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
Ctime *time.Time `json:"ctime"`
|
||||||
|
|
||||||
Req *Request `json:"req"`
|
Req *Request `json:"req"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
// Version is the build version, set at build time, using `go build -ldflags "-X github.com/GopeedLab/gopeed/pkg/base.Version=1.0.0"`.
|
||||||
|
var Version string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if Version == "" {
|
||||||
|
Version = "dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
+347
-166
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,6 @@ 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/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"reflect"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -37,7 +36,7 @@ func TestDownloader_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, rr.Res) {
|
if !test.AssertResourceEqual(want, rr.Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +83,82 @@ func TestDownloader_Create(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||||
|
// No proxy
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// Disable proxy
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||||
|
proxyCfg.Enable = false
|
||||||
|
return proxyCfg
|
||||||
|
})
|
||||||
|
// Invalid proxy scheme
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||||
|
proxyCfg.Scheme = ""
|
||||||
|
return proxyCfg
|
||||||
|
})
|
||||||
|
// Invalid proxy host
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||||
|
proxyCfg.Host = ""
|
||||||
|
return proxyCfg
|
||||||
|
})
|
||||||
|
// Use proxy without auth
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||||
|
return proxyCfg
|
||||||
|
})
|
||||||
|
// Use proxy with auth
|
||||||
|
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||||
|
return proxyCfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig) {
|
||||||
|
httpListener := test.StartTestFileServer()
|
||||||
|
defer httpListener.Close()
|
||||||
|
usr, pwd := "", ""
|
||||||
|
if auth {
|
||||||
|
usr, pwd = "admin", "123"
|
||||||
|
}
|
||||||
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
|
defer proxyListener.Close()
|
||||||
|
|
||||||
|
downloader := NewDownloader(nil)
|
||||||
|
if err := downloader.Setup(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer downloader.Clear()
|
||||||
|
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
})
|
||||||
|
|
||||||
|
req := &base.Request{
|
||||||
|
URL: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
|
||||||
|
}
|
||||||
|
rr, err := downloader.Resolve(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := &base.Resource{
|
||||||
|
Size: test.BuildSize,
|
||||||
|
Range: true,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: test.BuildName,
|
||||||
|
Path: "",
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !test.AssertResourceEqual(want, rr.Res) {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloader_CreateRename(t *testing.T) {
|
func TestDownloader_CreateRename(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -205,10 +280,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
|||||||
defer downloader.Clear()
|
defer downloader.Clear()
|
||||||
|
|
||||||
var httpCfg map[string]any
|
var httpCfg map[string]any
|
||||||
exits, err := downloader.getProtocolConfig("http", &httpCfg)
|
exits := downloader.getProtocolConfig("http", &httpCfg)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if exits {
|
if exits {
|
||||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
||||||
}
|
}
|
||||||
@@ -234,7 +306,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = downloader.PutConfig(storeCfg); err != nil {
|
if err := downloader.PutConfig(storeCfg); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ package engine
|
|||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/dop251/goja_nodejs/eventloop"
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
"github.com/dop251/goja_nodejs/process"
|
gojaurl "github.com/dop251/goja_nodejs/url"
|
||||||
"github.com/dop251/goja_nodejs/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed polyfill/out/index.js
|
//go:embed polyfill/out/index.js
|
||||||
@@ -27,12 +29,16 @@ type Engine struct {
|
|||||||
func (e *Engine) RunString(script string) (value any, err error) {
|
func (e *Engine) RunString(script string) (value any, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("%v", r)
|
err = r.(error)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var result goja.Value
|
var result goja.Value
|
||||||
e.loop.Run(func(runtime *goja.Runtime) {
|
e.loop.Run(func(runtime *goja.Runtime) {
|
||||||
result, err = runtime.RunString(script)
|
result, err = runtime.RunString(script)
|
||||||
|
if err == nil {
|
||||||
|
go e.await(result)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -45,9 +51,10 @@ func (e *Engine) RunString(script string) (value any, err error) {
|
|||||||
func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
|
func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err = fmt.Errorf("%v", r)
|
err = r.(error)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var result goja.Value
|
var result goja.Value
|
||||||
e.loop.Run(func(runtime *goja.Runtime) {
|
e.loop.Run(func(runtime *goja.Runtime) {
|
||||||
if args == nil {
|
if args == nil {
|
||||||
@@ -59,18 +66,57 @@ func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err err
|
|||||||
}
|
}
|
||||||
result, err = fn(nil, jsArgs...)
|
result, err = fn(nil, jsArgs...)
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
|
go e.await(result)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return
|
||||||
}
|
}
|
||||||
return resolveResult(result)
|
return resolveResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loop.Run will hang if the script result has a non-stop code, such as setInterval.
|
||||||
|
// This method will stop the event loop when the promise result is resolved.
|
||||||
|
func (e *Engine) await(value any) {
|
||||||
|
if value == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := value.(goja.Value); ok {
|
||||||
|
// if result is promise, wait for it to be resolved
|
||||||
|
if p, ok := v.Export().(*goja.Promise); ok {
|
||||||
|
if p.State() != goja.PromiseStatePending {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check promise state every 100 milliseconds, until it is resolved
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
if p.State() == goja.PromiseStatePending {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop the event loop
|
||||||
|
e.loop.StopNoWait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) Close() {
|
func (e *Engine) Close() {
|
||||||
e.loop.Stop()
|
e.loop.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEngine() *Engine {
|
type Config struct {
|
||||||
|
ProxyURL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEngine(cfg *Config) *Engine {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &Config{}
|
||||||
|
}
|
||||||
loop := eventloop.NewEventLoop()
|
loop := eventloop.NewEventLoop()
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
loop: loop,
|
loop: loop,
|
||||||
@@ -78,15 +124,18 @@ func NewEngine() *Engine {
|
|||||||
loop.Run(func(runtime *goja.Runtime) {
|
loop.Run(func(runtime *goja.Runtime) {
|
||||||
engine.Runtime = runtime
|
engine.Runtime = runtime
|
||||||
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||||
url.Enable(runtime)
|
vm.Enable(runtime)
|
||||||
process.Enable(runtime)
|
gojaurl.Enable(runtime)
|
||||||
|
if err := gojaerror.Enable(runtime); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := file.Enable(runtime); err != nil {
|
if err := file.Enable(runtime); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := formdata.Enable(runtime); err != nil {
|
if err := formdata.Enable(runtime); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := xhr.Enable(runtime); err != nil {
|
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := runtime.RunString(polyfillScript); err != nil {
|
if _, err := runtime.RunString(polyfillScript); err != nil {
|
||||||
@@ -96,13 +145,17 @@ func NewEngine() *Engine {
|
|||||||
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// polyfill window.location
|
||||||
|
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(script string) (value any, err error) {
|
func Run(script string) (value any, err error) {
|
||||||
engine := NewEngine()
|
engine := NewEngine(nil)
|
||||||
return engine.RunString(script)
|
return engine.RunString(script)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,8 +171,12 @@ func resolveResult(value goja.Value) (any, error) {
|
|||||||
case goja.PromiseStateFulfilled:
|
case goja.PromiseStateFulfilled:
|
||||||
return p.Result().Export(), nil
|
return p.Result().Export(), nil
|
||||||
case goja.PromiseStateRejected:
|
case goja.PromiseStateRejected:
|
||||||
|
if err, ok := p.Result().Export().(error); ok {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
return nil, errors.New(p.Result().String())
|
return nil, errors.New(p.Result().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return export, nil
|
return export, nil
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
|
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -19,16 +22,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestPolyfill(t *testing.T) {
|
func TestPolyfill(t *testing.T) {
|
||||||
|
doTestPolyfill(t, "MessageError")
|
||||||
doTestPolyfill(t, "XMLHttpRequest")
|
doTestPolyfill(t, "XMLHttpRequest")
|
||||||
doTestPolyfill(t, "Blob")
|
doTestPolyfill(t, "Blob")
|
||||||
doTestPolyfill(t, "FormData")
|
doTestPolyfill(t, "FormData")
|
||||||
doTestPolyfill(t, "fetch")
|
doTestPolyfill(t, "fetch")
|
||||||
|
doTestPolyfill(t, "__gopeed_create_vm")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
engine := NewEngine(nil)
|
||||||
|
_, err := engine.RunString(`
|
||||||
|
throw new MessageError('test');
|
||||||
|
`)
|
||||||
|
if me, ok := gojautil.AssertError[*gojaerror.MessageError](err); !ok {
|
||||||
|
t.Fatalf("expect MessageError, but got %v", me)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetch(t *testing.T) {
|
func TestFetch(t *testing.T) {
|
||||||
server := startServer()
|
server := startServer()
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
engine := NewEngine()
|
engine := NewEngine(nil)
|
||||||
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", server.Addr().String())); err != nil {
|
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", server.Addr().String())); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -191,6 +206,97 @@ function testTimeout(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetchWithProxy(t *testing.T) {
|
||||||
|
doTestFetchWithProxy(t, "", "")
|
||||||
|
doTestFetchWithProxy(t, "admin", "123")
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTestFetchWithProxy(t *testing.T, usr, pwd string) {
|
||||||
|
httpListener := startServer()
|
||||||
|
defer httpListener.Close()
|
||||||
|
|
||||||
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
|
defer proxyListener.Close()
|
||||||
|
|
||||||
|
engine := NewEngine(&Config{ProxyURL: util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)})
|
||||||
|
|
||||||
|
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respCode, err := engine.RunString(`
|
||||||
|
(async function(){
|
||||||
|
const resp = await fetch(host+'/get');
|
||||||
|
return resp.status;
|
||||||
|
})()
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if respCode != int64(200) {
|
||||||
|
t.Fatalf("fetch with proxy failed, want %d, got %d", 200, respCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVm(t *testing.T) {
|
||||||
|
engine := NewEngine(nil)
|
||||||
|
|
||||||
|
value, err := engine.RunString(`
|
||||||
|
const vm = __gopeed_create_vm()
|
||||||
|
vm.set('a', 1)
|
||||||
|
vm.set('b', 2)
|
||||||
|
const result = vm.runString('a=a+1;b=b+1;a+b;')
|
||||||
|
const out = {
|
||||||
|
"a": vm.get('a'),
|
||||||
|
"b": vm.get('b'),
|
||||||
|
"result": result
|
||||||
|
}
|
||||||
|
out
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := map[string]any{
|
||||||
|
"a": 2,
|
||||||
|
"b": 3,
|
||||||
|
"result": 5,
|
||||||
|
}
|
||||||
|
if !test.JsonEqual(value, want) {
|
||||||
|
t.Fatalf("vm test failed, want %v, got %v", want, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonStopLoop(t *testing.T) {
|
||||||
|
engine := NewEngine(nil)
|
||||||
|
|
||||||
|
_, err := engine.RunString(`
|
||||||
|
function leak(){
|
||||||
|
setInterval(() => {
|
||||||
|
},500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(){
|
||||||
|
leak()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve('done')
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := callTestFun(engine, "test")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if val != "done" {
|
||||||
|
t.Fatalf("infinite loop test failed, want %s, got %s", "done", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func doTestPolyfill(t *testing.T, module string) {
|
func doTestPolyfill(t *testing.T, module string) {
|
||||||
value, err := Run(fmt.Sprintf(`
|
value, err := Run(fmt.Sprintf(`
|
||||||
!!globalThis['%s']
|
!!globalThis['%s']
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package error
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageError struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MessageError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func Enable(runtime *goja.Runtime) error {
|
||||||
|
messageError := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
var message string
|
||||||
|
if len(call.Arguments) > 0 {
|
||||||
|
message = call.Arguments[0].String()
|
||||||
|
}
|
||||||
|
instance := &MessageError{
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
instanceValue := runtime.ToValue(instance).(*goja.Object)
|
||||||
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
|
return instanceValue
|
||||||
|
})
|
||||||
|
return runtime.Set("MessageError", messageError)
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package inject
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ThrowTypeError(vm *goja.Runtime, msg string) {
|
|
||||||
panic(vm.NewTypeError(msg))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package vm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Vm struct {
|
||||||
|
loop *eventloop.EventLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *Vm) Set(name string, value any) {
|
||||||
|
vm.loop.Run(func(runtime *goja.Runtime) {
|
||||||
|
runtime.Set(name, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *Vm) Get(name string) (value any) {
|
||||||
|
vm.loop.Run(func(runtime *goja.Runtime) {
|
||||||
|
value = runtime.Get(name)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm *Vm) RunString(script string) (value any, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = r.(error)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
vm.loop.Run(func(runtime *goja.Runtime) {
|
||||||
|
value, err = runtime.RunString(script)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Enable(runtime *goja.Runtime) error {
|
||||||
|
return runtime.Set("__gopeed_create_vm", func(call goja.FunctionCall) goja.Value {
|
||||||
|
return runtime.ToValue(&Vm{
|
||||||
|
loop: eventloop.NewEventLoop(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,14 +2,15 @@ package xhr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ type XMLHttpRequest struct {
|
|||||||
requestHeaders map[string]string
|
requestHeaders map[string]string
|
||||||
responseHeaders map[string]string
|
responseHeaders map[string]string
|
||||||
aborted bool
|
aborted bool
|
||||||
|
proxyUrl *url.URL
|
||||||
|
|
||||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
@@ -200,7 +202,12 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
for k, v := range xhr.requestHeaders {
|
for k, v := range xhr.requestHeaders {
|
||||||
req.Header.Set(k, v)
|
req.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
|
transport := &http.Transport{}
|
||||||
|
if xhr.proxyUrl != nil {
|
||||||
|
transport.Proxy = http.ProxyURL(xhr.proxyUrl)
|
||||||
|
}
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
@@ -306,10 +313,10 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
|
|||||||
return data.String()
|
return data.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Enable(runtime *goja.Runtime) error {
|
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
||||||
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
if len(call.Arguments) < 1 {
|
if len(call.Arguments) < 1 {
|
||||||
inject.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
|
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
|
||||||
}
|
}
|
||||||
instance := &ProgressEvent{
|
instance := &ProgressEvent{
|
||||||
Type: call.Argument(0).String(),
|
Type: call.Argument(0).String(),
|
||||||
@@ -320,6 +327,7 @@ func Enable(runtime *goja.Runtime) error {
|
|||||||
})
|
})
|
||||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
instance := &XMLHttpRequest{
|
instance := &XMLHttpRequest{
|
||||||
|
proxyUrl: proxyUrl,
|
||||||
Upload: &XMLHttpRequestUpload{
|
Upload: &XMLHttpRequestUpload{
|
||||||
EventProp: &EventProp{
|
EventProp: &EventProp{
|
||||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ThrowTypeError(vm *goja.Runtime, msg string) {
|
||||||
|
panic(vm.NewTypeError(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertError[T error](err error) (t T, r bool) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e, ok := err.(T); ok {
|
||||||
|
return e, true
|
||||||
|
}
|
||||||
|
if e, ok := err.(*goja.Exception); ok {
|
||||||
|
if ee, okk := e.Value().Export().(T); okk {
|
||||||
|
return ee, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
+125
-23
@@ -6,9 +6,12 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/logger"
|
"github.com/GopeedLab/gopeed/internal/logger"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine"
|
"github.com/GopeedLab/gopeed/pkg/download/engine"
|
||||||
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||||
|
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -34,7 +37,7 @@ type ActivationEvent string
|
|||||||
const (
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -195,11 +198,19 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
|
|||||||
if err := util.RmAndMkDirAll(tempExtDir); err != nil {
|
if err := util.RmAndMkDirAll(tempExtDir); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
proxyOptions := transport.ProxyOptions{}
|
||||||
|
proxyUrl := d.cfg.ProxyUrl()
|
||||||
|
if proxyUrl != nil {
|
||||||
|
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
|
||||||
|
proxyOptions.Username = proxyUrl.User.Username()
|
||||||
|
proxyOptions.Password, _ = proxyUrl.User.Password()
|
||||||
|
}
|
||||||
// clone project to extension temp dir
|
// clone project to extension temp dir
|
||||||
gitUrl := parentPath + projectPath + gitSuffix
|
gitUrl := parentPath + projectPath + gitSuffix
|
||||||
if _, err := git.PlainClone(tempExtDir, false, &git.CloneOptions{
|
if _, err := git.PlainClone(tempExtDir, false, &git.CloneOptions{
|
||||||
URL: gitUrl,
|
URL: gitUrl,
|
||||||
Depth: 1,
|
Depth: 1,
|
||||||
|
ProxyOptions: proxyOptions,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -229,8 +240,8 @@ func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) {
|
|||||||
return &ext, nil
|
return &ext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource, err error) {
|
||||||
doTrigger(d,
|
err = doTrigger(d,
|
||||||
EventOnResolve,
|
EventOnResolve,
|
||||||
req,
|
req,
|
||||||
&OnResolveContext{
|
&OnResolveContext{
|
||||||
@@ -243,7 +254,11 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
|||||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Res.CalcSize()
|
ctx.Res.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
|
||||||
|
for _, file := range ctx.Res.Files {
|
||||||
|
file.Name = util.ReplaceInvalidFilename(file.Name)
|
||||||
|
}
|
||||||
|
ctx.Res.CalcSize(nil)
|
||||||
}
|
}
|
||||||
res = ctx.Res
|
res = ctx.Res
|
||||||
},
|
},
|
||||||
@@ -256,7 +271,7 @@ func (d *Downloader) triggerOnStart(task *Task) {
|
|||||||
EventOnStart,
|
EventOnStart,
|
||||||
task.Meta.Req,
|
task.Meta.Req,
|
||||||
&OnStartContext{
|
&OnStartContext{
|
||||||
Task: task.clone(),
|
Task: NewExtensionTask(d, task),
|
||||||
},
|
},
|
||||||
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
|
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
|
||||||
// Validate request structure
|
// Validate request structure
|
||||||
@@ -265,15 +280,25 @@ func (d *Downloader) triggerOnStart(task *Task) {
|
|||||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
|
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Modify real task request
|
|
||||||
task.Meta.Req = ctx.Task.Meta.Req
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) {
|
func (d *Downloader) triggerOnError(task *Task, err error) {
|
||||||
|
doTrigger(d,
|
||||||
|
EventOnError,
|
||||||
|
task.Meta.Req,
|
||||||
|
&OnErrorContext{
|
||||||
|
Task: NewExtensionTask(d, task),
|
||||||
|
Error: err,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
Events: make(InstanceEvents),
|
Events: make(InstanceEvents),
|
||||||
@@ -315,7 +340,9 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
|
|||||||
if req.Labels == nil {
|
if req.Labels == nil {
|
||||||
req.Labels = make(map[string]string)
|
req.Labels = make(map[string]string)
|
||||||
}
|
}
|
||||||
engine := engine.NewEngine()
|
engine := engine.NewEngine(&engine.Config{
|
||||||
|
ProxyURL: d.cfg.ProxyUrl(),
|
||||||
|
})
|
||||||
defer engine.Close()
|
defer engine.Close()
|
||||||
err = engine.Runtime.Set("gopeed", gopeed)
|
err = engine.Runtime.Set("gopeed", gopeed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -333,13 +360,20 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
|
|||||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", ext.buildIdentity(), event)
|
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", ext.buildIdentity(), event)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if handler != nil {
|
||||||
handler(ext, gopeed, ctx)
|
handler(ext, gopeed, ctx)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
// Only return MessageError
|
||||||
|
if me, ok := gojautil.AssertError[*gojaerror.MessageError](err); ok {
|
||||||
|
return me
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) ExtensionPath(ext *Extension) string {
|
func (d *Downloader) ExtensionPath(ext *Extension) string {
|
||||||
@@ -422,8 +456,50 @@ func (e *Extension) update(newExt *Extension) error {
|
|||||||
e.Homepage = newExt.Homepage
|
e.Homepage = newExt.Homepage
|
||||||
e.Repository = newExt.Repository
|
e.Repository = newExt.Repository
|
||||||
e.Scripts = newExt.Scripts
|
e.Scripts = newExt.Scripts
|
||||||
// don't override settings
|
// merge settings
|
||||||
//e.Settings = newExt.Settings
|
// if new setting not exist in old settings, append it
|
||||||
|
for _, newSetting := range newExt.Settings {
|
||||||
|
var exist bool
|
||||||
|
for _, setting := range e.Settings {
|
||||||
|
if setting.Name == newSetting.Name {
|
||||||
|
exist = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
e.Settings = append(e.Settings, newSetting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if old setting not exist in new settings, remove it
|
||||||
|
for i := 0; i < len(e.Settings); i++ {
|
||||||
|
var exist bool
|
||||||
|
for _, setting := range newExt.Settings {
|
||||||
|
if setting.Name == e.Settings[i].Name {
|
||||||
|
exist = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
e.Settings = append(e.Settings[:i], e.Settings[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if new setting exist in old settings, update it
|
||||||
|
for _, newSetting := range newExt.Settings {
|
||||||
|
for _, setting := range e.Settings {
|
||||||
|
if setting.Name == newSetting.Name {
|
||||||
|
setting.Title = newSetting.Title
|
||||||
|
setting.Description = newSetting.Description
|
||||||
|
setting.Options = newSetting.Options
|
||||||
|
// if type changed, reset value
|
||||||
|
if setting.Type != newSetting.Type {
|
||||||
|
setting.Type = newSetting.Type
|
||||||
|
setting.Value = newSetting.Value
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
e.UpdatedAt = time.Now()
|
e.UpdatedAt = time.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -525,10 +601,10 @@ func (h InstanceEvents) OnStart(fn goja.Callable) {
|
|||||||
h.register(EventOnStart, fn)
|
h.register(EventOnStart, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (h InstanceEvents) OnError(fn goja.Callable) {
|
func (h InstanceEvents) OnError(fn goja.Callable) {
|
||||||
// h.register(HookEventOnError, 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(HookEventOnDone, fn)
|
||||||
//}
|
//}
|
||||||
@@ -557,28 +633,28 @@ type InstanceLogger struct {
|
|||||||
logger *logger.Logger
|
logger *logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *InstanceLogger) Debug(msg ...any) {
|
func (l *InstanceLogger) Debug(msg ...goja.Value) {
|
||||||
if l.devMode {
|
if l.devMode {
|
||||||
l.logger.Debug().Msg(l.append(msg...))
|
l.logger.Debug().Msg(l.append(msg...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *InstanceLogger) Info(msg ...any) {
|
func (l *InstanceLogger) Info(msg ...goja.Value) {
|
||||||
l.logger.Info().Msg(l.append(msg...))
|
l.logger.Info().Msg(l.append(msg...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *InstanceLogger) Warn(msg ...any) {
|
func (l *InstanceLogger) Warn(msg ...goja.Value) {
|
||||||
l.logger.Warn().Msg(l.append(msg...))
|
l.logger.Warn().Msg(l.append(msg...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *InstanceLogger) Error(msg ...any) {
|
func (l *InstanceLogger) Error(msg ...goja.Value) {
|
||||||
l.logger.Error().Msg(l.append(msg...))
|
l.logger.Error().Msg(l.append(msg...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *InstanceLogger) append(msg ...any) string {
|
func (l *InstanceLogger) append(msg ...goja.Value) string {
|
||||||
strMsg := make([]string, len(msg))
|
strMsg := make([]string, len(msg))
|
||||||
for i, m := range msg {
|
for i, m := range msg {
|
||||||
strMsg[i] = fmt.Sprint(m)
|
strMsg[i] = m.String()
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " "))
|
return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " "))
|
||||||
}
|
}
|
||||||
@@ -597,7 +673,33 @@ type OnResolveContext struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OnStartContext struct {
|
type OnStartContext struct {
|
||||||
Task *Task `json:"task"`
|
Task *ExtensionTask `json:"task"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnErrorContext struct {
|
||||||
|
Task *ExtensionTask `json:"task"`
|
||||||
|
Error error `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtensionTask struct {
|
||||||
|
*Task
|
||||||
|
|
||||||
|
download *Downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||||
|
return &ExtensionTask{
|
||||||
|
Task: task.clone(),
|
||||||
|
download: download,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtensionTask) Continue() error {
|
||||||
|
return t.download.Continue(t.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ExtensionTask) Pause() error {
|
||||||
|
return t.download.Pause(t.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSettings(settings []*Setting) map[string]any {
|
func parseSettings(settings []*Setting) map[string]any {
|
||||||
|
|||||||
+104
-27
@@ -4,6 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/GopeedLab/gopeed/internal/logger"
|
"github.com/GopeedLab/gopeed/internal/logger"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||||
|
"github.com/dop251/goja"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -95,6 +97,15 @@ func TestDownloader_InstallExtensionByGitFull(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloader_UpgradeExtension(t *testing.T) {
|
func TestDownloader_UpgradeExtension(t *testing.T) {
|
||||||
|
getSetting := func(settings []*Setting, name string) *Setting {
|
||||||
|
for _, setting := range settings {
|
||||||
|
if setting.Name == name {
|
||||||
|
return setting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
setupDownloader(func(downloader *Downloader) {
|
setupDownloader(func(downloader *Downloader) {
|
||||||
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false)
|
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,19 +133,40 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
|
|||||||
t.Fatal("extension update fail")
|
t.Fatal("extension update fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check setting update
|
||||||
|
s1 := getSetting(upgradeExt.Settings, "s1")
|
||||||
|
if s1.Title == "S1 old" {
|
||||||
|
t.Fatal("setting update fail")
|
||||||
|
}
|
||||||
|
// check setting type update
|
||||||
|
s2 := getSetting(upgradeExt.Settings, "s2")
|
||||||
|
if s2.Type == "number" {
|
||||||
|
t.Fatal("setting type update fail")
|
||||||
|
}
|
||||||
|
// check setting remove
|
||||||
|
d1 := getSetting(upgradeExt.Settings, "d1")
|
||||||
|
if d1 != nil {
|
||||||
|
t.Fatal("setting remove fail")
|
||||||
|
}
|
||||||
|
// check setting add
|
||||||
|
s3 := getSetting(upgradeExt.Settings, "s3")
|
||||||
|
if s3 == nil {
|
||||||
|
t.Fatal("setting add fail")
|
||||||
|
}
|
||||||
|
|
||||||
rr, err := downloader.Resolve(&base.Request{
|
rr, err := downloader.Resolve(&base.Request{
|
||||||
URL: "https://github.com/GopeedLab/gopeed/releases",
|
URL: "https://test.com",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(rr.Res.Files) == 1 {
|
if rr.Res.Name != "test" {
|
||||||
t.Fatal("resolve error")
|
t.Fatal("script update fail")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloader_ExtensionByOnStart(t *testing.T) {
|
func TestDownloader_Extension_OnStart(t *testing.T) {
|
||||||
downloadAndCheck := func(req *base.Request) {
|
downloadAndCheck := func(req *base.Request) {
|
||||||
setupDownloader(func(downloader *Downloader) {
|
setupDownloader(func(downloader *Downloader) {
|
||||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
|
||||||
@@ -161,10 +193,10 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
task := downloader.GetTask(id)
|
task := downloader.GetTask(id)
|
||||||
if task.Meta.Req.URL != "https://github.com" {
|
if task.Meta.Req.URL != "https://github.com" {
|
||||||
panic("extension on start modify url error")
|
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
||||||
}
|
}
|
||||||
if task.Meta.Req.Labels["modified"] != "true" {
|
if task.Meta.Req.Labels["modified"] != "true" {
|
||||||
panic("extension on start modify label error")
|
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -176,13 +208,54 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
|
|||||||
|
|
||||||
// label match
|
// label match
|
||||||
downloadAndCheck(&base.Request{
|
downloadAndCheck(&base.Request{
|
||||||
URL: "https://xxx.com",
|
URL: "https://test.com",
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"test": "true",
|
"test": "true",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDownloader_Extension_OnError(t *testing.T) {
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_error", 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/gopeed/test/404",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"test": "true",
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-errCh:
|
||||||
|
break
|
||||||
|
case <-time.After(time.Second * 10):
|
||||||
|
err = errors.New("timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("extension on error download error: " + err.Error())
|
||||||
|
}
|
||||||
|
// extension on error modify url and continue download
|
||||||
|
task := downloader.GetTask(id)
|
||||||
|
if task.Meta.Req.URL != "https://github.com" {
|
||||||
|
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
|
||||||
|
}
|
||||||
|
if task.Status != base.DownloadStatusDone {
|
||||||
|
t.Fatalf("except status is done, actual: %s", task.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDownloader_Extension_Errors(t *testing.T) {
|
func TestDownloader_Extension_Errors(t *testing.T) {
|
||||||
setupDownloader(func(downloader *Downloader) {
|
setupDownloader(func(downloader *Downloader) {
|
||||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil {
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil {
|
||||||
@@ -213,6 +286,26 @@ func TestDownloader_Extension_Errors(t *testing.T) {
|
|||||||
t.Fatal("function error catch failed")
|
t.Fatal("function error catch failed")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setupDownloader(func(downloader *Downloader) {
|
||||||
|
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/message_error", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err := downloader.Resolve(&base.Request{
|
||||||
|
URL: "https://github.com/test",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("except error, but got nil")
|
||||||
|
}
|
||||||
|
me, ok := err.(*gojaerror.MessageError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("except MessageError type, but got %s", err)
|
||||||
|
}
|
||||||
|
want := "test"
|
||||||
|
if me.Error() != want {
|
||||||
|
t.Fatalf("except MessageError message %s, but got %s", want, me.Message)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloader_Extension_Settings(t *testing.T) {
|
func TestDownloader_Extension_Settings(t *testing.T) {
|
||||||
@@ -305,31 +398,15 @@ func TestDownloader_DeleteExtension(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloader_Extension_OnResolve(t *testing.T) {
|
|
||||||
setupDownloader(func(downloader *Downloader) {
|
|
||||||
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := downloader.DeleteExtension(installedExt.Identity); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
extensions := downloader.GetExtensions()
|
|
||||||
if len(extensions) != 0 {
|
|
||||||
t.Fatal("extension delete fail")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDownloader_Extension_Logger(t *testing.T) {
|
func TestDownloader_Extension_Logger(t *testing.T) {
|
||||||
logger := logger.NewLogger(false, "")
|
logger := logger.NewLogger(false, "")
|
||||||
il := newInstanceLogger(&Extension{
|
il := newInstanceLogger(&Extension{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
}, logger)
|
}, logger)
|
||||||
il.Debug("msg1", "msg2")
|
il.Debug(goja.NaN(), goja.Undefined())
|
||||||
il.Info("msg1", "msg2")
|
il.Info(goja.NaN(), goja.Undefined())
|
||||||
il.Warn("msg1", "msg2")
|
il.Warn(goja.NaN(), goja.Undefined())
|
||||||
il.Error("msg1", "msg2")
|
il.Error(goja.NaN(), goja.Undefined())
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupDownloader(fn func(downloader *Downloader)) {
|
func setupDownloader(fn func(downloader *Downloader)) {
|
||||||
|
|||||||
+34
-3
@@ -7,7 +7,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -21,9 +23,10 @@ type Task struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||||
Status base.Status `json:"status"`
|
Status base.Status `json:"status"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
Progress *Progress `json:"progress"`
|
Progress *Progress `json:"progress"`
|
||||||
Size int64 `json:"size"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
|
||||||
fetcherBuilder fetcher.FetcherBuilder
|
fetcherBuilder fetcher.FetcherBuilder
|
||||||
fetcher fetcher.Fetcher
|
fetcher fetcher.Fetcher
|
||||||
@@ -41,17 +44,23 @@ func NewTask() *Task {
|
|||||||
ID: id,
|
ID: id,
|
||||||
Status: base.DownloadStatusReady,
|
Status: base.DownloadStatusReady,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Task) updateStatus(status base.Status) {
|
||||||
|
t.UpdatedAt = time.Now()
|
||||||
|
t.Status = status
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Task) clone() *Task {
|
func (t *Task) clone() *Task {
|
||||||
return &Task{
|
return &Task{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
Meta: t.Meta,
|
Meta: t.Meta,
|
||||||
Status: t.Status,
|
Status: t.Status,
|
||||||
Progress: t.Progress,
|
Progress: t.Progress,
|
||||||
Size: t.Size,
|
|
||||||
CreatedAt: t.CreatedAt,
|
CreatedAt: t.CreatedAt,
|
||||||
|
UpdatedAt: t.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,12 +115,34 @@ type DownloaderStoreConfig struct {
|
|||||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||||
Extra map[string]any `json:"extra"` // Extra is the extra config
|
Extra map[string]any `json:"extra"`
|
||||||
|
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||||
if cfg.MaxRunning == 0 {
|
if cfg.MaxRunning == 0 {
|
||||||
cfg.MaxRunning = 5
|
cfg.MaxRunning = 5
|
||||||
}
|
}
|
||||||
|
if cfg.Proxy == nil {
|
||||||
|
cfg.Proxy = &DownloaderProxyConfig{}
|
||||||
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *DownloaderStoreConfig) ProxyUrl() *url.URL {
|
||||||
|
if cfg.Proxy == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.Proxy.Enable == false || cfg.Proxy.Scheme == "" || cfg.Proxy.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return util.BuildProxyUrl(cfg.Proxy.Scheme, cfg.Proxy.Host, cfg.Proxy.Usr, cfg.Proxy.Pwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloaderProxyConfig struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Usr string `json:"usr"`
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
}
|
||||||
@@ -222,6 +222,9 @@ func (b *BoltStorage) Pop(bucket string, key string, v any) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return json.Unmarshal(data, v)
|
return json.Unmarshal(data, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
gopeed.events.onResolve(async function (ctx) {
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
const aaa = {};
|
const aaa = {};
|
||||||
|
// access undefined property
|
||||||
gopeed.logger.info(aaa.bbb.ccc);
|
gopeed.logger.info(aaa.bbb.ccc);
|
||||||
|
|
||||||
ctx.res = {
|
ctx.res = {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
gopeed.events.onResolve(async function (ctx) {
|
||||||
|
throw new MessageError("test");
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "message-error",
|
||||||
|
"title": "gopeed extension message error test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onResolve",
|
||||||
|
"match": {
|
||||||
|
"urls": [
|
||||||
|
"*://github.com/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
gopeed.events.onError(async function (ctx) {
|
||||||
|
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||||
|
gopeed.logger.info("error", ctx.error);
|
||||||
|
ctx.task.meta.req.url = "https://github.com";
|
||||||
|
ctx.task.pause();
|
||||||
|
ctx.task.continue();
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "on-error",
|
||||||
|
"title": "gopeed extension on error event test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"event": "onError",
|
||||||
|
"match": {
|
||||||
|
"labels": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry": "index.js"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loaded 50 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user