Compare commits

..
39 Commits
Author SHA1 Message Date
Levi 8c01e45780 release: v1.5.5 2024-01-30 14:50:29 +08:00
Levi 8ad571aa08 fix: config init fail (#365) 2024-01-30 14:49:26 +08:00
Levi 39316e9b8e fix: windows default install dir (#366) 2024-01-30 14:41:22 +08:00
liwei f778596390 release: v1.5.4 2024-01-30 10:57:08 +08:00
liwei c2cbc14492 docs: improve download guide 2024-01-30 10:37:27 +08:00
Levi 62c22bc6b4 feat: support use server time for file creation (#359) 2024-01-30 09:08:01 +08:00
Levi b89c7bc608 feat: migrate shared_preferences to hive (#343) 2024-01-24 15:49:51 +08:00
Tracey Lesley f2cd686563 fix: missing pb size format (#342) 2024-01-24 14:33:18 +08:00
Levi ef5ee99f04 feat: custom torrent client version (#341) 2024-01-23 17:44:44 +08:00
Tracey Lesley 6d136604db feat: downloader task stats (#339) 2024-01-22 11:43:12 +08:00
Levi 38ff711ea9 feat: add batch delete tasks api (#338) 2024-01-19 18:04:54 +08:00
liwei a1a8abc25f fix: ios download path 2024-01-19 15:45:24 +08:00
madoka773 abccbabc78 fix: wrong debian installed size (#335) 2024-01-19 11:17:50 +08:00
Levi c26eb26b8f fix: default download path on mobile (#337) 2024-01-19 11:12:27 +08:00
Levi 74beea6044 feat: auto create torrent task for http download (#334) 2024-01-18 18:38:44 +08:00
Levi e548d7e0c9 feat: save window size and resize on startup (#332) 2024-01-18 15:00:05 +08:00
madoka773 a996d57914 ci: fix debian 12 depends (#329) 2024-01-18 11:16:50 +08:00
Levi f7dc60ad50 fix: proxy setting only numeric input (#328) 2024-01-18 09:17:06 +08:00
Levi 8425a96ac9 feat: add mirror.ghproxy.com trackers subscribe cdn (#330) 2024-01-17 22:09:37 +08:00
Tracey Lesley 99f4e8c895 feat: add download stats info (#323) 2024-01-17 21:59:01 +08:00
Elliot cc0f61b527 feat: improve zh-TW translation (#327) 2024-01-17 18:16:40 +08:00
Levi 922ef969a6 feat: add thanks item for contributors (#325) 2024-01-16 19:02:20 +08:00
Tracey Lesley 3a542054fc fix: compatible with not follow the http range response standard 2024-01-14 09:58:14 +08:00
Levi 895a8d2ee4 fix: foreground task scope (#314) 2024-01-05 21:49:21 +08:00
madoka773 29d0f5ebcf ci: use flutter distributor (#313) 2024-01-04 12:46:16 +08:00
Levi 31228e3edf release: v1.5.3 2023-12-15 19:23:16 +08:00
Levi 31a32d7e14 fix: handle api panic (#302) 2023-12-15 19:20:56 +08:00
Levi 65fd6fb489 fix: http pause handle (#303) 2023-12-15 19:15:38 +08:00
Levi 953b878a89 fix: adapt mobile proxy settings ui (#301) 2023-12-15 19:09:48 +08:00
Elliot ae4ca3da58 feat: improve zh-TW translation (#300) 2023-12-14 18:06:49 +08:00
Levi d09bd180af release: v1.5.2 2023-12-13 09:38:34 +08:00
liwei 3d24e353c5 docs: docker data path mounted 2023-12-13 08:57:57 +08:00
Levi ce7489b0c0 feat: support volume storage dir on docker (#291) 2023-12-13 08:51:22 +08:00
Levi 2b5091ba21 feat: improve extension engine (#298)
1. support onError event
2. add built-in MessageError
2023-12-12 19:00:42 +08:00
Levi ff32971aca fix: repair invalid filename (#296) 2023-12-12 18:40:36 +08:00
Levi f2af61f9aa fix: sort of completed tasks (#297) 2023-12-12 18:36:40 +08:00
Elliot d935b9d631 feat: improve zh_TW translate (#287) 2023-12-04 23:01:49 +08:00
liwei ae66d1d83d docs: fix contributing 2023-12-04 13:41:27 +08:00
madoka773 aa91ca1d50 ci: linux build use libappindicator (#286) 2023-12-04 13:36:27 +08:00
88 changed files with 2085 additions and 1045 deletions

No files matched your search

+2
View File
@@ -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:
+60 -149
View File
@@ -7,14 +7,14 @@ on:
description: 'Build platform' description: 'Build platform'
required: true required: true
default: 'all' default: 'all'
channel: test:
description: 'Snap channel' description: 'Test mode'
required: true required: true
default: 'stable' default: "false"
env: env:
GO_VERSION: "1.19" GO_VERSION: "1.21"
FLUTTER_VERSION: "3.16.1" FLUTTER_VERSION: "3.16.8"
permissions: permissions:
contents: write contents: write
@@ -23,11 +23,11 @@ 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 }}
@@ -40,19 +40,20 @@ jobs:
- 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"
@@ -65,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 @"
@@ -73,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"
@@ -124,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:
@@ -143,9 +144,11 @@ 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-$GO_VERSION.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:
@@ -176,12 +179,12 @@ jobs:
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
@@ -203,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:
@@ -223,126 +226,32 @@ 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 build/dist flutter_distributor package --platform linux --targets appimage,deb
cd dist/*
# Build portable mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
tar -C build/linux/x64/release/bundle -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz ./ mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
# Build deb package
mkdir -p debian/gopeed/opt/gopeed
mkdir -p debian/gopeed/usr/share/applications
mkdir -p debian/gopeed/usr/share/icons/hicolor/scalable/apps
mkdir -p debian/gopeed/DEBIAN
cp -r build/linux/x64/release/bundle/* debian/gopeed/opt/gopeed/
cp assets/icon/icon.svg debian/gopeed/usr/share/icons/hicolor/scalable/apps/com.gopeed.Gopeed.svg
cat>debian/gopeed/DEBIAN/postinst<<EOF
#!/bin/bash
ln -sf /opt/gopeed/gopeed /usr/bin/gopeed
EOF
cat>debian/gopeed/DEBIAN/postrm<<EOF
#!/bin/bash
rm -rf /usr/bin/gopeed
EOF
chmod 0755 debian/gopeed/DEBIAN/postinst
chmod 0755 debian/gopeed/DEBIAN/postrm
cat>debian/gopeed/DEBIAN/control<<EOF
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
cp linux/assets/com.gopeed.Gopeed.desktop debian/gopeed/usr/share/applications/gopeed.desktop
dpkg-deb --build debian/gopeed
mv debian/gopeed.deb build/dist/Gopeed-$VERSION-linux-amd64.deb
# Build AppImage
sudo apt-get -y install libfuse2
wget -O appimage-builder https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder
mkdir AppDir
cp -r build/linux/x64/release/bundle/* AppDir
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/
cp assets/icon/icon.svg AppDir/usr/share/icons/hicolor/scalable/apps/gopeed.svg
cat>AppImageBuilder.yml<<EOF
version: 1
AppDir:
path: ${PWD}/AppDir
app_info:
id: com.gopeed.gopeed
name: Gopeed
icon: gopeed
version: ${VERSION:1}
exec: gopeed
exec_args: \$@
apt:
arch: amd64
allow_unauthenticated: true
sources:
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted
universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main
restricted universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports
main restricted universe multiverse
- sourceline: deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted
universe multiverse
include: []
files:
include: []
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
AppImage:
arch: x86_64
update-information: guess
EOF
./appimage-builder --skip-tests --recipe AppImageBuilder.yml
mv Gopeed-*-x86_64.AppImage build/dist/Gopeed-$VERSION-linux-amd64.AppImage
- 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/build/dist/* asset_path: ui/flutter/dist/*/*
overwrite: true overwrite: true
build-snap: build-snap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
@@ -362,9 +271,9 @@ jobs:
channel: latest/stable channel: latest/stable
- 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
sudo snap install snapcraft --classic sudo snap install snapcraft --classic
@@ -373,7 +282,7 @@ jobs:
cat>snap/snapcraft.yaml<<EOF cat>snap/snapcraft.yaml<<EOF
name: gopeed name: gopeed
version: ${VERSION:1} version: $VERSION
summary: A modern download manager summary: A modern download manager
description: High speed downloader that supports all platforms. description: High speed downloader that supports all platforms.
@@ -405,7 +314,7 @@ jobs:
plugin: flutter plugin: flutter
stage-packages: stage-packages:
- libgtk-3-dev - libgtk-3-dev
- libayatana-appindicator3-dev - libappindicator3-dev
flutter-target: lib/main.dart flutter-target: lib/main.dart
EOF EOF
@@ -416,7 +325,7 @@ jobs:
# Snapcraft login # Snapcraft login
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }} export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }}
snapcraft upload --release=${{ github.event.inputs.channel }} gopeed_${VERSION:1}_amd64.snap 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' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -435,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 }}
@@ -443,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:
@@ -469,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:
@@ -502,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
@@ -518,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 \
@@ -558,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 }}
+2 -2
View File
@@ -27,8 +27,8 @@ on:
- "go.sum" - "go.sum"
env: env:
GO_VERSION: "1.19" GO_VERSION: "1.21"
FLUTTER_VERSION: "3.16.1" FLUTTER_VERSION: "3.16.8"
jobs: jobs:
# lint: # lint:
+1 -1
View File
@@ -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 -1
View File
@@ -16,7 +16,7 @@ flutter 项目即可运行。
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。 Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
请注意以 `en_US.json` 为参照进行翻译。 请注意以 `en_us.dart` 为参照进行翻译。
## flutter开发 ## flutter开发
+1 -1
View File
@@ -15,7 +15,7 @@
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。 Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
請以 `en_US.json` 作為參照。 請以 `en_us.dart` 作為參照。
## flutter開發 ## flutter開發
+5 -3
View File
@@ -1,21 +1,23 @@
FROM instrumentisto/flutter:3.16.1 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"]
+67 -36
View File
@@ -15,24 +15,74 @@
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html). Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com) ## Download
## Install <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>
**Supported platforms** More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
- [x] windows
- [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
@@ -42,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!
@@ -76,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
@@ -160,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
+66 -42
View File
@@ -19,49 +19,74 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
## インストール ## インストール
**サポートするプラットフォーム** <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
``` ```
## 感謝 ## 感謝
+67 -35
View File
@@ -15,23 +15,74 @@
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。 Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/) 访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
## 安装 ## 下载
**已支持平台** <table>
<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),直译过来中文名叫做`够快下载器`(不
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
``` ```
## 感谢 ## 感谢
+66 -34
View File
@@ -17,21 +17,72 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
前往 ✈ [主頁](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`
### 使用CLI安裝 ### 使用CLI安裝
@@ -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
``` ```
## 感謝 ## 感謝
+9
View File
@@ -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
View File
@@ -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
+62 -53
View File
@@ -1,10 +1,10 @@
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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
@@ -12,97 +12,106 @@ require (
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/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.8 go.etcd.io/bbolt v1.3.8
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/sync v0.3.0 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.10.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-20231101202521-4ca4178f5c7a // 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/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.14.0 // indirect go.opentelemetry.io/otel/metric v1.22.0 // indirect
golang.org/x/mod v0.8.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect
golang.org/x/net v0.17.0 // indirect golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.6.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
) )
+179 -130
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -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.
+25
View File
@@ -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"
@@ -11,6 +12,7 @@ import (
"github.com/anacrolix/torrent/metainfo" "github.com/anacrolix/torrent/metainfo"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -63,6 +65,8 @@ func (f *Fetcher) initClient() (err error) {
} }
cfg := torrent.NewDefaultClientConfig() cfg := torrent.NewDefaultClientConfig()
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
cfg.ListenPort = f.config.ListenPort cfg.ListenPort = f.config.ListenPort
if f.ctl.ProxyUrl != nil { if f.ctl.ProxyUrl != nil {
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl) cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
@@ -128,6 +132,7 @@ func (f *Fetcher) Start() (err error) {
file.Download() file.Download()
} }
} }
return return
} }
@@ -188,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
@@ -207,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()),
@@ -295,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
}
+3 -2
View File
@@ -1,6 +1,7 @@
package http package http
type config struct { type config struct {
UserAgent string `json:"userAgent"` UserAgent string `json:"userAgent"`
Connections int `json:"connections"` Connections int `json:"connections"`
UseServerCtime bool `json:"useServerCtime"`
} }
+39 -20
View File
@@ -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"
) )
@@ -104,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))
@@ -129,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 != "" {
@@ -217,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 {
@@ -251,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
}() }()
} }
@@ -269,7 +290,7 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
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
@@ -306,26 +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 // Http request success, reset retry times
}() chunk.retryTimes = 0
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
}
// Http request success, reset retry times
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 {
@@ -342,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 {
+29 -9
View File
@@ -10,7 +10,6 @@ import (
"github.com/GopeedLab/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
"net" "net"
"reflect"
"testing" "testing"
"time" "time"
) )
@@ -69,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)
} }
} }
@@ -151,10 +150,34 @@ func TestFetcher_DownloadWithProxy(t *testing.T) {
downloadWithProxy(httpListener, proxyListener, t) downloadWithProxy(httpListener, proxyListener, t)
} }
func TestFetcher_Config(t *testing.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)
@@ -338,14 +361,11 @@ func buildFetcher() *Fetcher {
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{
Connections: 16,
}
newController.GetConfig = func(v any) bool { newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil { if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
return false return false
} }
return true return true
+10
View File
@@ -4,12 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/armon/go-socks5" "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"
@@ -337,3 +339,11 @@ func StartSocks5Server(usr, pwd string) net.Listener {
go server.Serve(listener) go server.Serve(listener)
return 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)
}
+4 -1
View File
@@ -16,10 +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 = "Usr-Agent" HttpHeaderUserAgent = "User-Agent"
HttpHeaderLastModified = "Last-Modified"
HttpHeaderBytes = "bytes"
HttpHeaderRangeFormat = "bytes=%d-%d" HttpHeaderRangeFormat = "bytes=%d-%d"
) )
+5 -3
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"time"
) )
// Request download request // Request download request
@@ -59,9 +60,10 @@ func (r *Resource) CalcSize(selectFiles []int) {
} }
type FileInfo struct { 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"`
} }
+10
View File
@@ -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"
}
}
+145 -36
View File
@@ -6,8 +6,11 @@ import (
"github.com/GopeedLab/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"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/protocol/http"
"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"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"github.com/virtuald/go-paniclog" "github.com/virtuald/go-paniclog"
"os" "os"
"path/filepath" "path/filepath"
@@ -32,7 +35,8 @@ const (
) )
var ( var (
ErrTaskNotFound = errors.New("task not found") ErrTaskNotFound = errors.New("task not found")
ErrUnSupportedProtocol = errors.New("unsupported protocol")
) )
type Listener func(event *Event) type Listener func(event *Event)
@@ -91,6 +95,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
} }
} }
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log")) d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log")) d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
if cfg.ProductionMode { if cfg.ProductionMode {
@@ -119,6 +124,11 @@ func (d *Downloader) Setup() error {
} }
// init default config // init default config
d.cfg.DownloaderStoreConfig.Init() d.cfg.DownloaderStoreConfig.Init()
for _, fb := range d.fetcherBuilders {
f := fb.Build()
d.setupFetcher(f)
}
// load tasks from storage // load tasks from storage
var tasks []*Task var tasks []*Task
if err = d.storage.List(bucketTask, &tasks); err != nil { if err = d.storage.List(bucketTask, &tasks); err != nil {
@@ -207,7 +217,7 @@ func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
if ok { if ok {
return fetchBuilder, nil return fetchBuilder, nil
} }
return nil, errors.New("unsupported protocol") return nil, ErrUnSupportedProtocol
} }
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) { func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
@@ -225,7 +235,10 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
return return
} }
res := d.triggerOnResolve(req) res, err := d.triggerOnResolve(req)
if err != nil {
return
}
if res != nil && len(res.Files) > 0 { if res != nil && len(res.Files) > 0 {
rr = &ResolveResult{ rr = &ResolveResult{
Res: res, Res: res,
@@ -441,6 +454,13 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
for i, t := range d.tasks { for i, t := range d.tasks {
if t.ID == id { if t.ID == id {
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...) d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
break
}
}
for i, t := range d.waitTasks {
if t.ID == id {
d.waitTasks = append(d.waitTasks[:i], d.waitTasks[i+1:]...)
break
} }
} }
}() }()
@@ -453,6 +473,56 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
return return
} }
func (d *Downloader) DeleteByStatues(statues []base.Status, force bool) (err error) {
deleteTasks := d.GetTasksByStatues(statues)
if len(deleteTasks) == 0 {
return
}
deleteIds := make([]string, 0)
for _, task := range deleteTasks {
deleteIds = append(deleteIds, task.ID)
}
func() {
d.lock.Lock()
defer d.lock.Unlock()
for _, id := range deleteIds {
for i, t := range d.tasks {
if t.ID == id {
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
break
}
}
for i, t := range d.waitTasks {
if t.ID == id {
d.waitTasks = append(d.waitTasks[:i], d.waitTasks[i+1:]...)
break
}
}
}
}()
for _, task := range deleteTasks {
err = d.doDelete(task, force)
if err != nil {
return
}
}
d.notifyRunning()
return
}
func (d *Downloader) Stats(id string) (sr any, err error) {
task := d.GetTask(id)
if task == nil {
return sr, ErrTaskNotFound
}
sr = task.fetcher.Stats()
return
}
func (d *Downloader) doDelete(task *Task, force bool) (err error) { func (d *Downloader) doDelete(task *Task, force bool) (err error) {
err = func() error { err = func() error {
if task.fetcher != nil { if task.fetcher != nil {
@@ -525,9 +595,6 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
Task: task, Task: task,
Err: err, Err: err,
}) })
if eventKey == EventKeyError {
d.emit(EventKeyFinally, task, err)
}
} }
} }
@@ -544,6 +611,22 @@ func (d *Downloader) GetTasks() []*Task {
return d.tasks return d.tasks
} }
func (d *Downloader) GetTasksByStatues(statues []base.Status) []*Task {
if len(statues) == 0 {
return d.tasks
}
tasks := make([]*Task, 0)
for _, task := range d.tasks {
for _, status := range statues {
if task.Status == status {
tasks = append(tasks, task)
break
}
}
}
return tasks
}
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) { func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
return d.cfg.DownloaderStoreConfig, nil return d.cfg.DownloaderStoreConfig, nil
} }
@@ -572,26 +655,56 @@ func (d *Downloader) getProtocolConfig(name string, v any) bool {
func (d *Downloader) watch(task *Task) { func (d *Downloader) watch(task *Task) {
err := task.fetcher.Wait() err := task.fetcher.Wait()
if err != nil { if err != nil {
task.Status = base.DownloadStatusError d.doOnError(task, err)
d.emit(EventKeyError, task, err) return
} else {
task.Progress.Used = task.timer.Used()
if task.Meta.Res.Size == 0 {
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
}
used := task.Progress.Used / int64(time.Second)
if used == 0 {
used = 1
}
totalSize := task.Meta.Res.Size
task.Progress.Speed = totalSize / used
task.Progress.Downloaded = totalSize
task.Status = base.DownloadStatusDone
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err)
} }
task.Progress.Used = task.timer.Used()
if task.Meta.Res.Size == 0 {
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
}
used := task.Progress.Used / int64(time.Second)
if used == 0 {
used = 1
}
totalSize := task.Meta.Res.Size
task.Progress.Speed = totalSize / used
task.Progress.Downloaded = totalSize
task.updateStatus(base.DownloadStatusDone)
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err)
d.notifyRunning() d.notifyRunning()
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
downloadFilePath := task.Meta.SingleFilepath()
if e.AutoTorrent && strings.HasSuffix(downloadFilePath, ".torrent") {
go func() {
_, err2 := d.CreateDirect(
&base.Request{
URL: downloadFilePath,
},
&base.Options{
Path: task.Meta.Opts.Path,
SelectFiles: make([]int, 0),
})
if err2 != nil {
d.Logger.Error().Err(err2).Msgf("auto create torrent task failed, task id: %s", task.ID)
}
}()
}
}
}
func (d *Downloader) doOnError(task *Task, err error) {
d.Logger.Warn().Err(err).Msgf("task download failed, task id: %s", task.ID)
task.updateStatus(base.DownloadStatusError)
d.triggerOnError(task, err)
if task.Status == base.DownloadStatusError {
d.emit(EventKeyError, task, err)
d.emit(EventKeyFinally, task, err)
d.notifyRunning()
}
} }
func (d *Downloader) restoreFetcher(task *Task) error { func (d *Downloader) restoreFetcher(task *Task) error {
@@ -656,9 +769,9 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
task := NewTask() task := NewTask()
task.fetcherBuilder = fb task.fetcherBuilder = fb
task.fetcher = fetcher task.fetcher = fetcher
task.Protocol = fetcher.Name()
task.Meta = fetcher.Meta() task.Meta = fetcher.Meta()
task.Progress = &Progress{} task.Progress = &Progress{}
task.Status = base.DownloadStatusReady
initTask(task) initTask(task)
if err = fetcher.Create(opts); err != nil { if err = fetcher.Create(opts); err != nil {
return return
@@ -697,25 +810,18 @@ func (d *Downloader) doStart(task *Task) (err error) {
} }
} }
cloneTask := task.clone()
isCreate := task.Status == base.DownloadStatusReady isCreate := task.Status == base.DownloadStatusReady
task.Status = base.DownloadStatusRunning
doStart := func() error { doStart := func() error {
task.lock.Lock() task.lock.Lock()
defer task.lock.Unlock() defer task.lock.Unlock()
req := d.triggerOnStart(cloneTask) d.triggerOnStart(task)
if req != nil { task.updateStatus(base.DownloadStatusRunning)
task.Meta.Req = req
task.fetcher.Meta().Req = req
}
if task.Meta.Res == nil { if task.Meta.Res == nil {
err := task.fetcher.Resolve(task.Meta.Req) err := task.fetcher.Resolve(task.Meta.Req)
if err != nil { if err != nil {
task.Status = base.DownloadStatusError
d.emit(EventKeyError, task, err)
return err return err
} }
task.Meta.Res = task.fetcher.Meta().Res task.Meta.Res = task.fetcher.Meta().Res
@@ -724,8 +830,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
if isCreate { if isCreate {
d.checkDuplicateLock.Lock() d.checkDuplicateLock.Lock()
defer d.checkDuplicateLock.Unlock() defer d.checkDuplicateLock.Unlock()
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
// check if the download file is duplicated and rename it automatically. // check if the download file is duplicated and rename it automatically.
if task.Meta.Res.Name != "" { if task.Meta.Res.Name != "" {
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
fullDirPath := task.Meta.FolderPath() fullDirPath := task.Meta.FolderPath()
newName, err := util.CheckDuplicateAndRename(fullDirPath) newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil { if err != nil {
@@ -733,6 +841,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
} }
task.Meta.Opts.Name = newName task.Meta.Opts.Name = newName
} else { } else {
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
fullFilePath := task.Meta.SingleFilepath() fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath) newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil { if err != nil {
@@ -758,7 +867,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
go func() { go func() {
err := doStart() err := doStart()
if err != nil { if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID) d.doOnError(task, err)
} }
}() }()
@@ -768,7 +877,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
func (d *Downloader) doPause(task *Task) (err error) { func (d *Downloader) doPause(task *Task) (err error) {
err = func() error { err = func() error {
if task.Status != base.DownloadStatusDone { if task.Status != base.DownloadStatusDone {
task.Status = base.DownloadStatusPause task.updateStatus(base.DownloadStatusPause)
task.timer.Pause() task.timer.Pause()
if task.fetcher != nil { if task.fetcher != nil {
if err := task.fetcher.Pause(); err != nil { if err := task.fetcher.Pause(); err != nil {
+2 -3
View File
@@ -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)
} }
} }
@@ -155,7 +154,7 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
}, },
}, },
} }
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)
} }
} }
+9 -1
View File
@@ -3,6 +3,7 @@ package engine
import ( import (
_ "embed" _ "embed"
"errors" "errors"
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/vm"
@@ -125,6 +126,9 @@ func NewEngine(cfg *Config) *Engine {
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime) vm.Enable(runtime)
gojaurl.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
} }
@@ -167,7 +171,11 @@ 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:
return nil, errors.New(p.Result().String()) if err, ok := p.Result().Export().(error); ok {
return nil, err
} else {
return nil, errors.New(p.Result().String())
}
} }
} }
return export, nil return export, nil
+13
View File
@@ -7,7 +7,9 @@ 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/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"io" "io"
@@ -20,6 +22,7 @@ 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")
@@ -27,6 +30,16 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "__gopeed_create_vm") 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()
@@ -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)
}
-9
View File
@@ -1,9 +0,0 @@
package inject
import (
"github.com/dop251/goja"
)
func ThrowTypeError(vm *goja.Runtime, msg string) {
panic(vm.NewTypeError(msg))
}
+2 -2
View File
@@ -2,9 +2,9 @@ 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"
@@ -316,7 +316,7 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) 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(),
+24
View File
@@ -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
}
+64 -14
View File
@@ -6,6 +6,8 @@ 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"
@@ -35,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"
) )
@@ -238,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{
@@ -252,6 +254,10 @@ 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.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
for _, file := range ctx.Res.Files {
file.Name = util.ReplaceInvalidFilename(file.Name)
}
ctx.Res.CalcSize(nil) ctx.Res.CalcSize(nil)
} }
res = ctx.Res res = ctx.Res
@@ -260,12 +266,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
return return
} }
func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) { func (d *Downloader) triggerOnStart(task *Task) {
doTrigger(d, doTrigger(d,
EventOnStart, EventOnStart,
task.Meta.Req, task.Meta.Req,
&OnStartContext{ &OnStartContext{
Task: task, Task: NewExtensionTask(d, task),
}, },
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) { func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
// Validate request structure // Validate request structure
@@ -274,14 +280,25 @@ func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
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
} }
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),
@@ -343,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
} }
handler(ext, gopeed, ctx) if handler != nil {
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 {
@@ -577,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)
//} //}
@@ -649,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 {
+66 -20
View File
@@ -4,6 +4,7 @@ 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" "github.com/dop251/goja"
"os" "os"
"testing" "testing"
@@ -165,7 +166,7 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
}) })
} }
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 {
@@ -192,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"])
} }
}) })
} }
@@ -207,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 {
@@ -244,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) {
@@ -336,22 +398,6 @@ 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{
+10
View File
@@ -7,6 +7,7 @@ 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" "net/url"
"sync" "sync"
@@ -22,8 +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"`
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,9 +44,15 @@ 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,
@@ -51,6 +60,7 @@ func (t *Task) clone() *Task {
Status: t.Status, Status: t.Status,
Progress: t.Progress, Progress: t.Progress,
CreatedAt: t.CreatedAt, CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
} }
} }
@@ -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"
}
]
}
+8
View File
@@ -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();
});
+16
View File
@@ -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"
}
]
}
+1
View File
@@ -1,4 +1,5 @@
gopeed.events.onStart(async function (ctx) { gopeed.events.onStart(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
ctx.task.meta.req.url = "https://github.com"; ctx.task.meta.req.url = "https://github.com";
ctx.task.meta.req.labels['modified'] = 'true'; ctx.task.meta.req.labels['modified'] = 'true';
}); });
+9
View File
@@ -3,3 +3,12 @@ package bt
type ReqExtra struct { type ReqExtra struct {
Trackers []string `json:"trackers"` Trackers []string `json:"trackers"`
} }
// Stats for download
type Stats struct {
// bt stats
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
TotalPeers int `json:"totalPeers"`
ActivePeers int `json:"activePeers"`
ConnectedSeeders int `json:"connectedSeeders"`
}
+8
View File
@@ -8,4 +8,12 @@ type ReqExtra struct {
type OptsExtra struct { type OptsExtra struct {
Connections int `json:"connections"` Connections int `json:"connections"`
// AutoTorrent when task download complete, and it is a .torrent file, it will be auto create a new task for the torrent file
AutoTorrent bool `json:"autoTorrent"`
}
// Stats for download
type Stats struct {
// http stats
// health indicators of http
} }
+36 -14
View File
@@ -104,6 +104,17 @@ func DeleteTask(w http.ResponseWriter, r *http.Request) {
WriteJson(w, model.NewNilResult()) WriteJson(w, model.NewNilResult())
} }
func DeleteTasks(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
status := r.Form["status"]
force := r.FormValue("force")
if err := Downloader.DeleteByStatues(convertStatues(status), force == "true"); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func GetTask(w http.ResponseWriter, r *http.Request) { func GetTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
taskId := vars["id"] taskId := vars["id"]
@@ -122,20 +133,8 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
func GetTasks(w http.ResponseWriter, r *http.Request) { func GetTasks(w http.ResponseWriter, r *http.Request) {
r.ParseForm() r.ParseForm()
status := r.Form["status"] status := r.Form["status"]
tasks := Downloader.GetTasks() tasks := Downloader.GetTasksByStatues(convertStatues(status))
if len(status) == 0 { WriteJson(w, model.NewOkResult(tasks))
WriteJson(w, model.NewOkResult(tasks))
return
}
result := make([]*download.Task, 0)
for _, task := range tasks {
for _, s := range status {
if task.Status == base.Status(s) {
result = append(result, task)
}
}
}
WriteJson(w, model.NewOkResult(result))
} }
func GetConfig(w http.ResponseWriter, r *http.Request) { func GetConfig(w http.ResponseWriter, r *http.Request) {
@@ -282,6 +281,29 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
w.Write(buf) w.Write(buf)
} }
func GetStats(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
if taskId == "" {
WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return
}
statsResult, err := Downloader.Stats(taskId)
if err != nil {
writeError(w, err.Error())
return
}
WriteJson(w, model.NewOkResult(statsResult))
}
func convertStatues(statues []string) []base.Status {
result := make([]base.Status, 0)
for _, status := range statues {
result = append(result, base.Status(status))
}
return result
}
func writeError(w http.ResponseWriter, msg string) { func writeError(w http.ResponseWriter, msg string) {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(msg)) w.Write([]byte(msg))
+18
View File
@@ -3,11 +3,13 @@ package rest
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors"
"net" "net"
"net/http" "net/http"
"os" "os"
@@ -93,8 +95,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask) r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask) r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask) r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
r.Methods(http.MethodDelete).Path("/api/v1/tasks").HandlerFunc(DeleteTasks)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask) r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks) r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}/stats").HandlerFunc(GetStats)
r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig) r.Methods(http.MethodGet).Path("/api/v1/config").HandlerFunc(GetConfig)
r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig) r.Methods(http.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension) r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
@@ -131,6 +135,20 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
}) })
} }
// recover panic
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
err := errors.WithStack(fmt.Errorf("%v", v))
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
}
}()
h.ServeHTTP(w, r)
})
})
srv = &http.Server{Handler: handlers.CORS( srv = &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}), handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
+55 -1
View File
@@ -66,7 +66,7 @@ var (
func TestResolve(t *testing.T) { func TestResolve(t *testing.T) {
doTest(func() { doTest(func() {
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq) resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
if !test.JsonEqual(taskRes, resp.Res) { if !test.AssertResourceEqual(taskRes, resp.Res) {
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes)) t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes))
} }
}) })
@@ -204,12 +204,14 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
// continue all // continue all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil) httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
time.Sleep(time.Millisecond * 100)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil) tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
if len(tasks) != cfg.MaxRunning { if len(tasks) != cfg.MaxRunning {
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning) t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
} }
// pause all // pause all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil) httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
time.Sleep(time.Millisecond * 100)
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil) tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
if len(tasks) != total { if len(tasks) != total {
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total) t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
@@ -240,6 +242,58 @@ func TestDeleteTaskForce(t *testing.T) {
}) })
} }
func TestDeleteAllTasks(t *testing.T) {
doTest(func() {
taskCount := 3
var wg sync.WaitGroup
wg.Add(taskCount)
Downloader.Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally {
wg.Done()
}
})
for i := 0; i < taskCount; i++ {
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
}
wg.Wait()
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks?force=true", nil)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
if len(tasks) != 0 {
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
}
})
}
func TestDeleteTasksByStatues(t *testing.T) {
doTest(func() {
taskCount := 3
var wg sync.WaitGroup
wg.Add(taskCount)
Downloader.Listener(func(event *download.Event) {
if event.Key == download.EventKeyFinally {
wg.Done()
}
})
for i := 0; i < taskCount; i++ {
httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
}
wg.Wait()
httpRequestCheckOk[any](http.MethodDelete, fmt.Sprintf("/api/v1/tasks?status=%s&force=true", base.DownloadStatusDone), nil)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
if len(tasks) != 0 {
t.Errorf("DeleteTasks() got = %v, want %v", len(tasks), 0)
}
})
}
func TestGetTasks(t *testing.T) { func TestGetTasks(t *testing.T) {
doTest(func() { doTest(func() {
var wg sync.WaitGroup var wg sync.WaitGroup
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"math" "math"
) )
var unitArr = []string{"B", "KB", "MB", "GB", "TB", "EB"} var unitArr = []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
func ByteFmt(size int64) string { func ByteFmt(size int64) string {
if size == 0 { if size == 0 {
+11
View File
@@ -183,3 +183,14 @@ func IsExistsFile(path string) bool {
} }
return false return false
} }
// ReplaceInvalidFilename replace invalid path characters
func ReplaceInvalidFilename(path string) string {
if path == "" {
return ""
}
for _, char := range invalidPathChars {
path = strings.ReplaceAll(path, char, "_")
}
return path
}
+6
View File
@@ -0,0 +1,6 @@
//go:build !windows
// +build !windows
package util
var invalidPathChars = []string{`/`, `:`}
+47
View File
@@ -178,3 +178,50 @@ func TestIsExistsFile(t *testing.T) {
}) })
} }
} }
func TestReplaceInvalidFilename(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want string
}{
{
name: "blank",
args: args{
path: "",
},
want: "",
},
{
name: "normal",
args: args{
path: "test.txt",
},
want: "test.txt",
},
{
name: "case1",
args: args{
path: "te/st.txt",
},
want: "te_st.txt",
},
{
name: "case2",
args: args{
path: "te/st:.txt",
},
want: "te_st_.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ReplaceInvalidFilename(tt.args.path); got != tt.want {
t.Errorf("ReplaceInvalidFilename() = %v, want %v", got, tt.want)
}
})
}
}
Loaded 50 of 88 files, more files were not shown because too many files have changed in this diff. Show more