mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9692e76e5 | ||
|
|
0a9e39ed75 | ||
|
|
1971f582b5 | ||
|
|
c62d6be3da | ||
|
|
7d5e503894 | ||
|
|
6c4066fde4 | ||
|
|
ffdf3e717f | ||
|
|
f66814b4ae | ||
|
|
0c3e201a51 | ||
|
|
c32a5d750f | ||
|
|
207c287c97 | ||
|
|
56ef25065e | ||
|
|
bd6d6be71f | ||
|
|
1d3a028bae | ||
|
|
27011fb96b | ||
|
|
c696b59c4b | ||
|
|
78b5b8a78b | ||
|
|
c81ca7f5cd | ||
|
|
e39d50fe09 | ||
|
|
086269f8bd | ||
|
|
00d2183961 | ||
|
|
5f914dc4c4 | ||
|
|
9bf204cf04 | ||
|
|
a816b9ccc8 | ||
|
|
d60d370463 | ||
|
|
84d054b494 | ||
|
|
1aa96e147f | ||
|
|
8c01e45780 | ||
|
|
8ad571aa08 | ||
|
|
39316e9b8e | ||
|
|
f778596390 | ||
|
|
c2cbc14492 | ||
|
|
62c22bc6b4 | ||
|
|
b89c7bc608 | ||
|
|
f2cd686563 | ||
|
|
ef5ee99f04 | ||
|
|
6d136604db | ||
|
|
38ff711ea9 | ||
|
|
a1a8abc25f | ||
|
|
abccbabc78 | ||
|
|
c26eb26b8f | ||
|
|
74beea6044 | ||
|
|
e548d7e0c9 | ||
|
|
a996d57914 | ||
|
|
f7dc60ad50 | ||
|
|
8425a96ac9 | ||
|
|
99f4e8c895 | ||
|
|
cc0f61b527 | ||
|
|
922ef969a6 | ||
|
|
3a542054fc | ||
|
|
895a8d2ee4 | ||
|
|
29d0f5ebcf | ||
|
|
31228e3edf | ||
|
|
31a32d7e14 | ||
|
|
65fd6fb489 | ||
|
|
953b878a89 | ||
|
|
ae4ca3da58 | ||
|
|
d09bd180af | ||
|
|
3d24e353c5 | ||
|
|
ce7489b0c0 | ||
|
|
2b5091ba21 | ||
|
|
ff32971aca | ||
|
|
f2af61f9aa | ||
|
|
d935b9d631 | ||
|
|
ae66d1d83d | ||
|
|
aa91ca1d50 | ||
|
|
2d4bdea5cb | ||
|
|
2f2393ac20 | ||
|
|
5a761e244a | ||
|
|
6538766780 | ||
|
|
16b7a3d5b3 | ||
|
|
ef97528114 | ||
|
|
b58669bb46 | ||
|
|
51d28c5e53 | ||
|
|
db15650664 | ||
|
|
ee7da179c1 | ||
|
|
6bb32372ab | ||
|
|
7d6428c967 | ||
|
|
bba5107dd5 | ||
|
|
ddfe76b635 | ||
|
|
aad7395dde | ||
|
|
6f541715df | ||
|
|
848797b2fd | ||
|
|
0a05bf41e7 | ||
|
|
485656d1d4 | ||
|
|
c074e0c2bc | ||
|
|
0db0a2c987 | ||
|
|
b06a79b5c7 |
No files matched your search
@@ -16,9 +16,11 @@ categories:
|
||||
- title: '🧰 Maintenance'
|
||||
label: 'chore'
|
||||
- title: '📖 Document'
|
||||
label: 'doc'
|
||||
label: 'docs'
|
||||
- title: '🚀 CI/CD'
|
||||
label: 'ci'
|
||||
- title: '🌎 Internationalization'
|
||||
label: 'translation'
|
||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||
version-resolver:
|
||||
major:
|
||||
@@ -32,7 +34,7 @@ version-resolver:
|
||||
- 'patch'
|
||||
default: patch
|
||||
template: |
|
||||
# Changes
|
||||
# Release notes
|
||||
|
||||
$CHANGES
|
||||
|
||||
|
||||
+92
-169
@@ -7,10 +7,14 @@ on:
|
||||
description: 'Build platform'
|
||||
required: true
|
||||
default: 'all'
|
||||
test:
|
||||
description: 'Test mode'
|
||||
required: true
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.19"
|
||||
FLUTTER_VERSION: "3.10.6"
|
||||
GO_VERSION: "1.21"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -19,11 +23,11 @@ jobs:
|
||||
get-release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag_name: ${{ steps.get-release.outputs.tag_name }}
|
||||
version: ${{ steps.get-release.outputs.version }}
|
||||
upload_url: ${{ steps.get-release.outputs.upload_url }}
|
||||
|
||||
steps:
|
||||
- uses: monkeyWie/get-latest-release@v1
|
||||
- uses: monkeyWie/get-latest-release@v2.1
|
||||
id: get-release
|
||||
with:
|
||||
myToken: ${{ github.token }}
|
||||
@@ -36,23 +40,24 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.20"
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
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\"
|
||||
|
||||
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
|
||||
$TAG = "v$env:VERSION"
|
||||
flutter build windows
|
||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||
$system = "C:\Windows\System32"
|
||||
$release = "build\windows\runner\Release\"
|
||||
$release = "build\windows\x64\runner\Release\"
|
||||
cp $mingw\libstdc++-6.dll $release
|
||||
cp $mingw\libgcc_s_seh-1.dll $release
|
||||
cp $mingw\libwinpthread-1.dll $release
|
||||
@@ -61,7 +66,7 @@ jobs:
|
||||
cp $system\vcruntime140_1.dll $release
|
||||
|
||||
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
|
||||
echo @"
|
||||
@@ -69,7 +74,7 @@ jobs:
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "Gopeed"
|
||||
#define MyAppVersion "$($env:VERSION.substring(1))"
|
||||
#define MyAppVersion "$env:VERSION"
|
||||
#define MyAppPublisher "monkeyWie"
|
||||
#define MyAppURL "https://gopeed.com"
|
||||
#define MyAppExeName "gopeed.exe"
|
||||
@@ -106,7 +111,7 @@ jobs:
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
||||
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[UninstallDelete]
|
||||
@@ -120,9 +125,9 @@ jobs:
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
"@ > setup.iss
|
||||
iscc.exe setup.iss
|
||||
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
||||
Compress-Archive -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe" -DestinationPath "Output\Gopeed-$env:VERSION-windows-amd64.zip"
|
||||
Remove-Item -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
||||
mv "Output\gopeed.exe" "Output\Gopeed-$TAG-windows-amd64.exe"
|
||||
Compress-Archive -Path "Output\Gopeed-$TAG-windows-amd64.exe" -DestinationPath "Output\Gopeed-$TAG-windows-amd64.zip"
|
||||
Remove-Item -Path "Output\Gopeed-$TAG-windows-amd64.exe"
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
@@ -139,9 +144,11 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
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
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -172,12 +179,12 @@ jobs:
|
||||
path: ui/flutter/lib-arm64
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
PROJECT_DIR=$(pwd)
|
||||
|
||||
# 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
|
||||
mkdir -p ui/flutter/macos/Frameworks
|
||||
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
|
||||
@@ -199,7 +206,7 @@ jobs:
|
||||
}
|
||||
EOF
|
||||
mkdir dist
|
||||
appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
|
||||
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
@@ -208,7 +215,7 @@ jobs:
|
||||
overwrite: true
|
||||
build-linux:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -219,135 +226,63 @@ jobs:
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||
sudo apt update -y
|
||||
# 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
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||
flutter_distributor package --platform linux --targets appimage,deb
|
||||
cd dist/*
|
||||
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
|
||||
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||
asset_path: ui/flutter/dist/*/*
|
||||
overwrite: true
|
||||
build-snap:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Setup LXD
|
||||
uses: canonical/[email protected]
|
||||
with:
|
||||
channel: latest/stable
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
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
|
||||
flutter build linux
|
||||
mkdir -p build/dist
|
||||
|
||||
# Build portable
|
||||
tar -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz build/linux/x64/release/bundle
|
||||
|
||||
|
||||
# Build deb package
|
||||
mkdir -p debian/gui
|
||||
cp assets/icon/icon_1024.png debian/gui/gopeed.png
|
||||
cat>debian/debian.yaml<<EOF
|
||||
flutter_app:
|
||||
command: gopeed
|
||||
arch: x64
|
||||
parent: /usr/local/bin
|
||||
control:
|
||||
Package: gopeed
|
||||
Version: ${VERSION:1}
|
||||
Architecture: amd64
|
||||
Essential: no
|
||||
Priority: optional
|
||||
Depends:
|
||||
Maintainer: monkeyWie
|
||||
Description: High speed downloader that supports all platforms.
|
||||
EOF
|
||||
|
||||
cat>debian/gui/gopeed.desktop<<EOF
|
||||
[Desktop Entry]
|
||||
Version=${VERSION:1}
|
||||
Name=Gopeed
|
||||
GenericName=Gopeed
|
||||
Comment=High speed downloader that supports all platforms.
|
||||
Exec=gopeed
|
||||
Icon=gopeed.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility
|
||||
Keywords=Flutter;Downloader;
|
||||
EOF
|
||||
|
||||
dart pub global activate flutter_to_debian
|
||||
flutter_to_debian
|
||||
mv debian/packages/gopeed*.deb 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
|
||||
cp assets/icon/icon_1024.png AppDir/usr/share/icons/gopeed.png
|
||||
cat>AppImageBuilder.yml<<EOF
|
||||
version: 1
|
||||
AppDir:
|
||||
path: ${PWD}/AppDir
|
||||
app_info:
|
||||
id: com.gopeed.gopeed
|
||||
name: Gopeed
|
||||
icon: gopeed.png
|
||||
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
|
||||
|
||||
# Build snap
|
||||
sudo snap install snapcraft --classic
|
||||
rm -rf build/linux
|
||||
mkdir -p snap/gui
|
||||
cp assets/icon/icon_1024.png snap/gui/gopeed.png
|
||||
cp assets/icon/icon.svg snap/gui/gopeed.svg
|
||||
|
||||
cat>snap/snapcraft.yaml<<EOF
|
||||
name: gopeed
|
||||
version: ${VERSION:1}
|
||||
version: $VERSION
|
||||
summary: A modern download manager
|
||||
description: High speed downloader that supports all platforms.
|
||||
|
||||
@@ -378,35 +313,19 @@ jobs:
|
||||
source: .
|
||||
plugin: flutter
|
||||
stage-packages:
|
||||
- libayatana-appindicator3-dev
|
||||
- libgtk-3-dev
|
||||
- libappindicator3-dev
|
||||
flutter-target: lib/main.dart
|
||||
EOF
|
||||
|
||||
cat>snap/gui/gopeed.desktop<<EOF
|
||||
[Desktop Entry]
|
||||
Version=${VERSION:1}
|
||||
Name=Gopeed
|
||||
GenericName=Gopeed
|
||||
Comment=High speed downloader that supports all platforms.
|
||||
Exec=gopeed
|
||||
Icon=\${SNAP}/meta/gui/gopeed.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility
|
||||
Keywords=Flutter;Downloader;
|
||||
EOF
|
||||
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
|
||||
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
|
||||
|
||||
snapcraft --use-lxd
|
||||
|
||||
|
||||
# Snapcraft login
|
||||
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }}
|
||||
snapcraft upload --release=stable gopeed_${VERSION:1}_amd64.snap
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||
asset_path: ui/flutter/build/dist/*
|
||||
overwrite: true
|
||||
snapcraft upload --release=${{ github.event.inputs.test == 'true' && 'edge' || 'stable' }} gopeed_${VERSION}_amd64.snap
|
||||
build-android:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -425,7 +344,7 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
APK_KEYSTORE: ${{ secrets.APK_KEYSTORE }}
|
||||
APK_KEY_PASSWORD: ${{ secrets.APK_KEY_PASSWORD }}
|
||||
APK_STORE_PASSWORD: ${{ secrets.APK_STORE_PASSWORD }}
|
||||
@@ -433,12 +352,12 @@ jobs:
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
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
|
||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||
flutter build apk
|
||||
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
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
@@ -459,19 +378,19 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
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
|
||||
flutter build ios --no-codesign
|
||||
mkdir Payload
|
||||
cp -r build/ios/iphoneos/Runner.app Payload
|
||||
zip -r -y Payload.zip Payload/Runner.app
|
||||
mkdir dist
|
||||
mv Payload.zip dist/Gopeed-$VERSION-ios.ipa
|
||||
mv Payload.zip dist/Gopeed-v$VERSION-ios.ipa
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
@@ -492,13 +411,14 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --web-renderer html
|
||||
cd ../../
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
mkdir -p dist/zip
|
||||
|
||||
goos_arr=(windows darwin linux)
|
||||
goarch_arr=(386 amd64 arm64)
|
||||
export CGO_ENABLED=0
|
||||
@@ -508,9 +428,9 @@ jobs:
|
||||
if [ $goos = "darwin" ]; then
|
||||
goos_name="macos"
|
||||
fi
|
||||
name=gopeed-web-$VERSION-$goos_name-$goarch
|
||||
name=gopeed-web-v$VERSION-$goos_name-$goarch
|
||||
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 \
|
||||
&& file=$(ls -AU | head -1) \
|
||||
&& mkdir $name \
|
||||
@@ -548,15 +468,18 @@ jobs:
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
with:
|
||||
push: true
|
||||
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
|
||||
build-args: |
|
||||
VERSION=${{ env.VERSION }}
|
||||
IN_DOCKER=true
|
||||
platforms: |
|
||||
linux/386
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
linux/arm/v6
|
||||
linux/arm/v7
|
||||
linux/arm64/v8
|
||||
tags: |
|
||||
liwei2633/gopeed:latest
|
||||
liwei2633/gopeed:${{ env.VERSION }}
|
||||
liwei2633/gopeed:v${{ env.VERSION }}
|
||||
@@ -27,8 +27,8 @@ on:
|
||||
- "go.sum"
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.19"
|
||||
FLUTTER_VERSION: "3.10.6"
|
||||
GO_VERSION: "1.21"
|
||||
FLUTTER_VERSION: "3.16.8"
|
||||
|
||||
jobs:
|
||||
# lint:
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
ui/flutter/dist/
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
|
||||
+2
-23
@@ -17,17 +17,9 @@ start the front-end flutter project in `debug` mode to run.
|
||||
|
||||
## Translation
|
||||
|
||||
The internationalization files of Gopeed are located in the `ui/flutter/assets/locales` directory.
|
||||
The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n/langs` directory.
|
||||
You only need to add the corresponding language file in this directory.
|
||||
|
||||
|
||||
Generate locales after you edit locales:
|
||||
|
||||
|
||||
```
|
||||
get generate locales
|
||||
```
|
||||
|
||||
Please refer to `en_us.dart` for translation.
|
||||
|
||||
## flutter development
|
||||
@@ -38,17 +30,4 @@ Turn on build_runner watcher if you want to edit api/models:
|
||||
|
||||
```
|
||||
flutter pub run build_runner watch
|
||||
```
|
||||
|
||||
|
||||
get-cli commands usages:
|
||||
|
||||
```
|
||||
create:
|
||||
controller: Generate controller
|
||||
page: Use to generate pages
|
||||
view: Generate view
|
||||
generate:
|
||||
locales: Generate translation file from json files
|
||||
model: generate Class model from json
|
||||
```
|
||||
```
|
||||
+1
-22
@@ -17,17 +17,9 @@
|
||||
|
||||
## 翻訳
|
||||
|
||||
Gopeed の国際化ファイルは `ui/flutter/assets/locales` ディレクトリにあります。
|
||||
Gopeed の国際化ファイルは `ui/flutter/lib/i18n/langs` ディレクトリにあります。
|
||||
このディレクトリに対応する言語ファイルを追加するだけでよいです。
|
||||
|
||||
|
||||
ロケール編集後にロケールを生成:
|
||||
|
||||
|
||||
```
|
||||
get generate locales
|
||||
```
|
||||
|
||||
翻訳については `en_us.dart` を参照してください。
|
||||
|
||||
## flutter での開発
|
||||
@@ -39,16 +31,3 @@ api/models を編集したい場合は build_runner watcher をオンにしま
|
||||
```
|
||||
flutter pub run build_runner watch
|
||||
```
|
||||
|
||||
|
||||
get-cli コマンドの使用法:
|
||||
|
||||
```
|
||||
create:
|
||||
controller: Generate controller
|
||||
page: Use to generate pages
|
||||
view: Generate view
|
||||
generate:
|
||||
locales: Generate translation file from json files
|
||||
model: generate Class model from json
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# Hướng dẫn đóng góp cho Gopeed
|
||||
|
||||
Trước tiên, cảm ơn bạn đã quan tâm đến việc đóng góp cho Gopeed. Hướng dẫn này sẽ giúp bạn tham gia
|
||||
phát triển Gopeed một cách tốt hơn.
|
||||
|
||||
## Mô tả nhánh
|
||||
|
||||
Dự án này chỉ có một nhánh chính duy nhất, đó là nhánh `main`. Nếu bạn muốn tham gia vào
|
||||
phát triển Gopeed, hãy fork dự án này trước, sau đó phát triển trong dự án fork của bạn. Sau khi
|
||||
hoàn thành phát triển, gửi một PR đến dự án này và merge vào nhánh `main`.
|
||||
|
||||
## Phát triển cục bộ
|
||||
|
||||
Đề nghị phát triển và gỡ lỗi thông qua web. Đầu tiên, khởi động dịch vụ backend bằng cách chạy
|
||||
lệnh `go run cmd/api/main.go` trong dòng lệnh, cổng mặc định của dịch vụ là `9999`, sau đó
|
||||
khởi động dự án flutter frontend trong chế độ `debug` để chạy.
|
||||
|
||||
## Dịch thuật
|
||||
|
||||
Các tệp quốc tế hóa của Gopeed được đặt trong thư mục `ui/flutter/lib/i18n/langs`.
|
||||
Bạn chỉ cần thêm tệp ngôn ngữ tương ứng trong thư mục này.
|
||||
|
||||
Vui lòng tham khảo `en_us.dart` để biết cách dịch thuật.
|
||||
|
||||
## Phát triển flutter
|
||||
|
||||
Đừng quên chạy `dart format ./ui/flutter` trước khi commit để giữ mã của bạn theo định dạng dart chuẩn.
|
||||
|
||||
Bật build_runner watcher nếu bạn muốn chỉnh sửa api/models:
|
||||
+2
-20
@@ -14,15 +14,9 @@ flutter 项目即可运行。
|
||||
|
||||
## 翻译
|
||||
|
||||
Gopeed 的国际化文件位于 `ui/flutter/assets/locales` 目录下,只需要在该目录下添加对应的语言文件即可。
|
||||
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
|
||||
|
||||
编辑locales后请运行以下命令:
|
||||
|
||||
```
|
||||
get generate locales
|
||||
```
|
||||
|
||||
请注意以 `en_US.json` 为参照进行翻译。
|
||||
请注意以 `en_us.dart` 为参照进行翻译。
|
||||
|
||||
## flutter开发
|
||||
|
||||
@@ -34,15 +28,3 @@ get generate locales
|
||||
flutter pub run build_runner watch
|
||||
```
|
||||
|
||||
适用get-cli 命令:
|
||||
|
||||
```
|
||||
create:
|
||||
controller: Generate controller
|
||||
page: Use to generate pages
|
||||
view: Generate view
|
||||
generate:
|
||||
locales: Generate translation file from json files
|
||||
model: generate Class model from json
|
||||
```
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Gopeed 協助指南
|
||||
|
||||
首先感謝您願意幫助我們改進並優化該項目,這份指南將會幫助您更好的參與 Gopeed 的開發。
|
||||
|
||||
## 分支說明
|
||||
|
||||
本項目只有一個分支,即 `main` 分支,如果您想要參與 Gopeed 的開發,請先 fork 該項目,再在您自己的 fork 中進行開發,開發完成後再開啟PR,以合併至 `main` 分支。
|
||||
|
||||
## 離線開發
|
||||
|
||||
建議使用 web 端進行開發與調試,首先啟動服務,使用指令 `go run cmd/api/main.go` 啟動 ,該服務默認連接埠為 `9999`,接著以 `debug` 模式啟動前端 flutter 項目即可。
|
||||
|
||||
## 翻譯
|
||||
|
||||
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
|
||||
|
||||
|
||||
請以 `en_us.dart` 作為參照。
|
||||
|
||||
## flutter開發
|
||||
|
||||
每次提交PR前請務必執行 `dart format ./ui/flutter`
|
||||
|
||||
如果需要編輯 api/models,請打開build_runner watcher:
|
||||
|
||||
```
|
||||
flutter pub run build_runner watch
|
||||
```
|
||||
+15
-4
@@ -1,21 +1,32 @@
|
||||
FROM instrumentisto/flutter:3.10.6 AS flutter
|
||||
FROM instrumentisto/flutter:3.16.8 AS flutter
|
||||
WORKDIR /app
|
||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
||||
RUN flutter pub get
|
||||
COPY ./ui/flutter ./
|
||||
RUN flutter build web --web-renderer html
|
||||
|
||||
FROM golang:1.19.3 AS go
|
||||
FROM golang:1.21.6 AS go
|
||||
WORKDIR /app
|
||||
COPY ./go.mod ./go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
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 -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
||||
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
||||
|
||||
FROM alpine:3.14.2
|
||||
LABEL maintainer="monkeyWie"
|
||||
WORKDIR /app
|
||||
COPY --from=go /app/dist/gopeed ./
|
||||
COPY entrypoint.sh ./entrypoint.sh
|
||||
RUN apk update && \
|
||||
apk upgrade --no-cache && \
|
||||
apk add --no-cache bash su-exec; \
|
||||
chmod +x ./entrypoint.sh && \
|
||||
rm -rf /var/cache/apk/*
|
||||
VOLUME ["/app/storage"]
|
||||
ENV PUID=0 PGID=0 UMASK=022
|
||||
EXPOSE 9999
|
||||
ENTRYPOINT ["./gopeed"]
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
@@ -9,30 +9,80 @@
|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
|
||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||
|
||||
## Introduction
|
||||
|
||||
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
|
||||
|
||||
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-macos.dmg">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan=4>Linux</td>
|
||||
<td><code>Flathub</code></td>
|
||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>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**
|
||||
|
||||
- [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
|
||||
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||
|
||||
### Command tool
|
||||
|
||||
@@ -42,24 +92,6 @@ use `go install`:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
1. Golang 1.19+
|
||||
2. Flutter 3.10+
|
||||
1. Golang 1.21+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### Clone
|
||||
|
||||
@@ -136,7 +168,7 @@ command:
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -160,7 +192,6 @@ cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||
go run cmd/web/main.go
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
+68
-44
@@ -9,7 +9,7 @@
|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
|
||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||
|
||||
## はじめに
|
||||
|
||||
@@ -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-macos.dmg">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan=4>Linux</td>
|
||||
<td><code>Flathub</code></td>
|
||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>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
|
||||
- [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` からウェブページにアクセスできます。
|
||||
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||
|
||||
## 寄付
|
||||
|
||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
|
||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||
|
||||
## ショーケース
|
||||
|
||||
@@ -75,8 +100,8 @@ docker コンテナが起動しているときは、`http://localhost:9999` か
|
||||
|
||||
### 環境
|
||||
|
||||
1. Go 言語 1.19+
|
||||
2. Flutter 3.10+
|
||||
1. Go 言語 1.21+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### クローン
|
||||
|
||||
@@ -135,7 +160,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -159,7 +184,6 @@ cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||
go run cmd/web/main.go
|
||||
```
|
||||
|
||||
## 感謝
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||

|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
|
||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||
|
||||
## Giới thiệu
|
||||
|
||||
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
|
||||
|
||||
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
|
||||
|
||||
## Tải về
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nền tảng</th>
|
||||
<th>Loại gói</th>
|
||||
<th>Liên kết tải xuống</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan=2>Windows</td>
|
||||
<td><code>Bộ cài đặt EXE</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ZIP Portable</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MacOS</td>
|
||||
<td><code>Bộ cài đặt DMG</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan=4>Linux</td>
|
||||
<td><code>Flathub</code></td>
|
||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SNAP</code></td>
|
||||
<td><a href="https://snapcraft.io/gopeed">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DEB</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>AppImage</code></td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Liên kết</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">Liên kết</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">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web</td>
|
||||
<td></td>
|
||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Liên kết</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker</td>
|
||||
<td></td>
|
||||
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Liên kết</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
|
||||
|
||||
### Command tool
|
||||
|
||||
Sử dụng `go install`:
|
||||
|
||||
```bash
|
||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
```
|
||||
|
||||
## Quyên góp
|
||||
|
||||
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
||||
|
||||
## Trưng bày
|
||||
|
||||

|
||||
|
||||
## Development
|
||||
|
||||
Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`.
|
||||
|
||||
> Mã giao diện nằm trong thư mục `ui/flutter`.
|
||||
|
||||
### Environment
|
||||
|
||||
1. Golang 1.21+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### Clone
|
||||
|
||||
```bash
|
||||
git clone [email protected]:GopeedLab/gopeed.git
|
||||
```
|
||||
|
||||
### Đóng góp
|
||||
|
||||
Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md)
|
||||
|
||||
### Xây dựng
|
||||
|
||||
#### Desktop
|
||||
|
||||
Trước tiên, bạn cần cấu hình môi trường theo tài liệu chính thức của [Tài liệu trang web máy tính để bàn Flutter](https://docs.flutter.dev/development/platform-integration/desktop), sau đó bạn cần đảm bảo môi trường cgo được thiết lập đúng. Để biết hướng dẫn chi tiết về cách thiết lập môi trường cgo, vui lòng tham khảo các tài liệu tương ứng có sẵn trực tuyến.
|
||||
|
||||
command:
|
||||
|
||||
- windows
|
||||
|
||||
```bash
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build windows
|
||||
```
|
||||
|
||||
- macos
|
||||
|
||||
```bash
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build macos
|
||||
```
|
||||
|
||||
- linux
|
||||
|
||||
```bash
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build linux
|
||||
```
|
||||
|
||||
#### Mobile
|
||||
|
||||
Giống như trước đây, bạn cũng cần chuẩn bị môi trường `cgo` và sau đó cài đặt `gomobile`:
|
||||
|
||||
```bash
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
gomobile init
|
||||
```
|
||||
|
||||
command:
|
||||
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
- ios
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build ios --no-codesign
|
||||
```
|
||||
|
||||
#### Web
|
||||
|
||||
command:
|
||||
|
||||
```bash
|
||||
cd ui/flutter
|
||||
flutter build web
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||
```
|
||||
|
||||
## Tín dụng
|
||||
|
||||
### Người đóng góp
|
||||
|
||||
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||
</a>
|
||||
|
||||
### JetBrains
|
||||
|
||||
[](https://www.jetbrains.com/?from=gopeed)
|
||||
|
||||
## Giấy phép
|
||||
|
||||
[GPLv3](LICENSE)
|
||||
+70
-38
@@ -9,29 +9,80 @@
|
||||
|
||||
[](https://ko-fi.com/R6R6IJGN6)
|
||||
|
||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||
|
||||
## 介绍
|
||||
|
||||
Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
|
||||
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
|
||||
|
||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
|
||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
|
||||
|
||||
## 安装
|
||||
## 下载
|
||||
|
||||
**已支持平台**
|
||||
<table>
|
||||
<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-macos.dmg">前往</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan=4>Linux</td>
|
||||
<td><code>Flathub</code></td>
|
||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>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
|
||||
- [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` 命令
|
||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||
|
||||
### 命令行工具
|
||||
|
||||
@@ -41,24 +92,6 @@ Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下
|
||||
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)以支持这个项目的发展,非常感谢!
|
||||
@@ -75,8 +108,8 @@ docker-compose up -d
|
||||
|
||||
### 环境要求
|
||||
|
||||
1. Golang 1.19+
|
||||
2. Flutter 3.10+
|
||||
1. Golang 1.21+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### 克隆项目
|
||||
|
||||
@@ -135,7 +168,7 @@ gomobile init
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
@@ -159,7 +192,6 @@ cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||
go run cmd/web/main.go
|
||||
```
|
||||
|
||||
## 感谢
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://github.com/GopeedLab/gopeed/releases)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||

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

|
||||
|
||||
## 開發
|
||||
|
||||
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
|
||||
|
||||
> 前端代碼位於`ui/flutter`目錄內。
|
||||
|
||||
### 開發環境
|
||||
|
||||
1. Golang 1.21+
|
||||
2. Flutter 3.16+
|
||||
|
||||
### 克隆項目
|
||||
|
||||
```bash
|
||||
git clone [email protected]:GopeedLab/gopeed.git
|
||||
```
|
||||
|
||||
### 協助開發
|
||||
|
||||
請參考[協助指南](CONTRIBUTING_zh-TW.md)
|
||||
|
||||
### 編譯
|
||||
|
||||
#### 桌面端
|
||||
|
||||
首先需要按照[flutter desktop 官方文檔](https://docs.flutter.dev/development/platform-integration/desktop)配置開發環境,並準備好`cgo`環境,具體方法可以自行搜索。
|
||||
|
||||
組建指令:
|
||||
|
||||
- windows
|
||||
|
||||
```bash
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build windows
|
||||
```
|
||||
|
||||
- macos
|
||||
|
||||
```bash
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build macos
|
||||
```
|
||||
|
||||
- linux
|
||||
|
||||
```bash
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build linux
|
||||
```
|
||||
|
||||
#### 移動設備
|
||||
|
||||
需要`cgo`環境,並安裝`gomobile`:
|
||||
|
||||
```bash
|
||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||
go get golang.org/x/mobile/bind
|
||||
gomobile init
|
||||
```
|
||||
|
||||
組建指令:
|
||||
|
||||
- android
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build apk
|
||||
```
|
||||
|
||||
- ios
|
||||
|
||||
```bash
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build ios --no-codesign
|
||||
```
|
||||
|
||||
#### 網頁端
|
||||
|
||||
組建指令:
|
||||
|
||||
```bash
|
||||
cd ui/flutter
|
||||
flutter build web
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||
```
|
||||
|
||||
## 感謝
|
||||
|
||||
### 貢獻者
|
||||
|
||||
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||
</a>
|
||||
|
||||
### JetBrains
|
||||
|
||||
[](https://www.jetbrains.com/?from=gopeed)
|
||||
|
||||
## 軟體許可
|
||||
|
||||
該軟體遵循 [GPLv3](LICENSE) 。
|
||||
+4
-3
@@ -8,9 +8,10 @@ import (
|
||||
// only for local development
|
||||
func main() {
|
||||
cfg := &model.StartConfig{
|
||||
Network: "tcp",
|
||||
Address: "127.0.0.1:9999",
|
||||
Storage: model.StorageBolt,
|
||||
Network: "tcp",
|
||||
Address: "127.0.0.1:9999",
|
||||
Storage: model.StorageBolt,
|
||||
WebEnable: true,
|
||||
}
|
||||
cmd.Start(cfg)
|
||||
}
|
||||
+12
-5
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"net/http"
|
||||
@@ -24,14 +25,20 @@ func Start(cfg *model.StartConfig) {
|
||||
panic(err)
|
||||
}
|
||||
if downloadCfg.FirstLoad {
|
||||
// Set default download dir to user download dir
|
||||
userDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
downloadDir := filepath.Join(userDir, "Downloads")
|
||||
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
|
||||
var downloadDir string
|
||||
if base.InDocker == "true" {
|
||||
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||
} else {
|
||||
userDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
downloadDir = filepath.Join(userDir, "Downloads")
|
||||
}
|
||||
}
|
||||
if downloadDir != "" {
|
||||
downloadCfg.DownloadDir = downloadDir
|
||||
rest.Downloader.PutConfig(downloadCfg)
|
||||
}
|
||||
|
||||
}
|
||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
|
||||
+11
-6
@@ -9,11 +9,12 @@ import (
|
||||
)
|
||||
|
||||
type args struct {
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
|
||||
configPath *string
|
||||
}
|
||||
@@ -23,8 +24,9 @@ func parse() *args {
|
||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Password")
|
||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
|
||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
flag.Parse()
|
||||
|
||||
@@ -45,6 +47,9 @@ func parse() *args {
|
||||
if cfgArgs.ApiToken == nil {
|
||||
cfgArgs.ApiToken = cliArgs.ApiToken
|
||||
}
|
||||
if cfgArgs.StorageDir == nil {
|
||||
cfgArgs.StorageDir = cliArgs.StorageDir
|
||||
}
|
||||
return cfgArgs
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
@@ -29,10 +31,22 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var dir string
|
||||
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||
dir = *args.StorageDir
|
||||
} else {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dir = filepath.Dir(exe)
|
||||
}
|
||||
|
||||
cfg := &model.StartConfig{
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: filepath.Join(dir, "storage"),
|
||||
ApiToken: *args.ApiToken,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
|
||||
+3
-2
@@ -4,8 +4,9 @@ services:
|
||||
gopeed:
|
||||
container_name: gopeed
|
||||
ports:
|
||||
- 9999:9999 # HTTP port (host:container)
|
||||
- "9999:9999" # HTTP port (host:container)
|
||||
image: liwei2633/gopeed
|
||||
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
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
chown -R ${PUID}:${PGID} /app/Download
|
||||
|
||||
umask ${UMASK}
|
||||
|
||||
exec su-exec ${PUID}:${PGID} ./gopeed "$@"
|
||||
@@ -1,107 +1,117 @@
|
||||
module github.com/GopeedLab/gopeed
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
|
||||
github.com/anacrolix/torrent v1.52.5
|
||||
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562
|
||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
||||
github.com/anacrolix/torrent v1.53.3
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
||||
github.com/go-git/go-git/v5 v5.8.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6
|
||||
golang.org/x/sync v0.3.0
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||
golang.org/x/sync v0.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // 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/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||
github.com/anacrolix/chansync v0.3.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
|
||||
github.com/anacrolix/envpprof v1.2.1 // indirect
|
||||
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // indirect
|
||||
github.com/anacrolix/chansync v0.4.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
||||
github.com/anacrolix/envpprof v1.3.0 // indirect
|
||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // 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/perf v1.0.0 // indirect
|
||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||
github.com/anacrolix/multiless v0.3.0 // indirect
|
||||
github.com/anacrolix/stm v0.4.0 // indirect
|
||||
github.com/anacrolix/sync v0.4.0 // indirect
|
||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
||||
github.com/anacrolix/utp v0.1.0 // indirect
|
||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
||||
github.com/anacrolix/stm v0.5.0 // indirect
|
||||
github.com/anacrolix/sync v0.5.1 // indirect
|
||||
github.com/anacrolix/upnp v0.1.3 // indirect
|
||||
github.com/anacrolix/utp v0.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/benbjohnson/immutable v0.3.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.2 // indirect
|
||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.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/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-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/pion/datachannel v1.5.2 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.4 // indirect
|
||||
github.com/pion/ice/v2 v2.2.6 // indirect
|
||||
github.com/pion/interceptor v0.1.11 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||
github.com/pion/ice/v2 v2.3.12 // indirect
|
||||
github.com/pion/interceptor v0.1.25 // 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/rtcp v1.2.9 // indirect
|
||||
github.com/pion/rtp v1.7.13 // indirect
|
||||
github.com/pion/sctp v1.8.2 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.5 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.9 // indirect
|
||||
github.com/pion/stun v0.3.5 // indirect
|
||||
github.com/pion/transport v0.13.1 // indirect
|
||||
github.com/pion/transport/v2 v2.0.0 // indirect
|
||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||
github.com/pion/udp v0.1.4 // indirect
|
||||
github.com/pion/webrtc/v3 v3.1.42 // indirect
|
||||
github.com/pion/rtcp v1.2.13 // indirect
|
||||
github.com/pion/rtp v1.8.3 // indirect
|
||||
github.com/pion/sctp v1.8.9 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
|
||||
github.com/rs/zerolog v1.31.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // 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/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
|
||||
go.opentelemetry.io/otel v1.8.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.8.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
golang.org/x/crypto v0.18.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/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.40.6 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/sqlite v1.28.0 // indirect
|
||||
zombiezen.com/go/sqlite v1.1.0 // indirect
|
||||
)
|
||||
@@ -1,14 +1,10 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw=
|
||||
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c h1:wvzox0eLO6CKQAMcOqz7oH3UFqMpMmK7kwmwV+22HIs=
|
||||
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
@@ -18,8 +14,8 @@ github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjA
|
||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
|
||||
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
|
||||
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
|
||||
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
||||
@@ -34,26 +30,26 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
|
||||
github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
||||
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
|
||||
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew=
|
||||
github.com/anacrolix/chansync v0.4.0 h1:Md0HM7zYCAO4KwNwgcIRgxNsMxiRuk7D1Ha0Uo+2y60=
|
||||
github.com/anacrolix/chansync v0.4.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
|
||||
github.com/anacrolix/dht/v2 v2.21.0 h1:8nzI+faaynY9jOKmVgdmBZVrTo8B7ZE/LKEgN3Vl/Bs=
|
||||
github.com/anacrolix/dht/v2 v2.21.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
|
||||
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
||||
github.com/anacrolix/envpprof v1.2.1 h1:25TJe6t/i0AfzzldiGFKCpD+s+dk8lONBcacJZB2rdE=
|
||||
github.com/anacrolix/envpprof v1.2.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
||||
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 h1:fyXlBfnlFzZSFckJ8QLb2lfmWfY++4RiUnae7ZMuv0A=
|
||||
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||
github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
|
||||
github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
|
||||
github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so=
|
||||
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
|
||||
github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
|
||||
github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
|
||||
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
|
||||
github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
|
||||
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
|
||||
github.com/anacrolix/log v0.14.0 h1:mYhTSemILe/Z8tIxbGdTIWWpPspI8W/fhZHpoFbDaL0=
|
||||
github.com/anacrolix/log v0.14.0/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
||||
github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
||||
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 h1:CdVK9IoqoqklXQQ4+L2aew64xsz14KdOD+rnKdTQajg=
|
||||
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
|
||||
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
|
||||
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
|
||||
github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
|
||||
@@ -66,46 +62,44 @@ github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy
|
||||
github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
|
||||
github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
|
||||
github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
|
||||
github.com/anacrolix/missinggo/v2 v2.5.2/go.mod h1:yNvsLrtZYRYCOI+KRH/JM8TodHjtIE/bjOGhQaLOWIE=
|
||||
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 h1:W/oGeHhYwxueeiDjQfmK9G+X9M2xJgfTtow62v0TWAs=
|
||||
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
|
||||
github.com/anacrolix/missinggo/v2 v2.7.3 h1:Ee//CmZBMadeNiYB/hHo9ly2PFOEZ4Fhsbnug3rDAIE=
|
||||
github.com/anacrolix/missinggo/v2 v2.7.3/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
|
||||
github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
|
||||
github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
|
||||
github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
|
||||
github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis=
|
||||
github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4=
|
||||
github.com/anacrolix/publicip v0.2.0/go.mod h1:67G1lVkLo8UjdEcJkwScWVTvlJ35OCDsRJoWXl/wi4g=
|
||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 h1:lOtCD+LzoD1g7bowhYJNR++uV+FyY5bTZXKwnPex9S8=
|
||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
|
||||
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
|
||||
github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY=
|
||||
github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8=
|
||||
github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
|
||||
github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
|
||||
github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
|
||||
github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
||||
github.com/anacrolix/sync v0.4.0 h1:T+MdO/u87ir/ijWsTFsPYw5jVm0SMm4kVpg8t4KF38o=
|
||||
github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
||||
github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc=
|
||||
github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
|
||||
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
|
||||
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||
github.com/anacrolix/tagflag v1.3.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
|
||||
github.com/anacrolix/torrent v1.52.5 h1:jWowdx+EU6zFVfBwmnL0d3H4J6vTFEGOrHI35YdfIT8=
|
||||
github.com/anacrolix/torrent v1.52.5/go.mod h1:CcM8oPMYye5J42cSqJrmUpqwRFgSsJQ1jCEHwygqnqQ=
|
||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
|
||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
|
||||
github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
|
||||
github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
|
||||
github.com/anacrolix/torrent v1.53.3 h1:Abb3J8p/JWUjOzHaii8kixuDOGMXz+Oh8+hwc/zA2lA=
|
||||
github.com/anacrolix/torrent v1.53.3/go.mod h1:is8GNob5qDeZ5Kq+pKPiE2xqYUi1ms7IgSB+CftZETk=
|
||||
github.com/anacrolix/upnp v0.1.3 h1:NlYEhE75adz2npEJKjbqyqnyW9qU4STookvSNXBJ5ao=
|
||||
github.com/anacrolix/upnp v0.1.3/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
||||
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
||||
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||
github.com/benbjohnson/immutable v0.3.0 h1:TVRhuZx2wG9SZ0LRdqlbs9S5BZ6Y24hJEHTCgWHZEIw=
|
||||
github.com/benbjohnson/immutable v0.3.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||
github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA=
|
||||
github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
|
||||
github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
|
||||
@@ -124,33 +118,34 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog=
|
||||
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
|
||||
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 h1:ObbB2tzHWWAxzsG5futqeq2Ual2zYlo/+eMkSc5sn8w=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562/go.mod h1:X2TOTJ+Uamd454RFp7ig2tmP3hQg0Z2Qk8gbVQmU0mk=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c h1:hLoodLRD4KLWIH8eyAQCLcH8EqIrjac7fCkp/fHnvuQ=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0=
|
||||
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
|
||||
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
|
||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
|
||||
github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||
@@ -169,11 +164,15 @@ github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+
|
||||
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-llsqlite/adapter v0.1.0 h1:wGSQNsu/rtYeu/lqZNZQMjwUdEF3OW66xTLvsFwJQUw=
|
||||
github.com/go-llsqlite/adapter v0.1.0/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
|
||||
github.com/go-llsqlite/crawshaw v0.5.0 h1:Olbqkth53vkkh4WvmkYjrtfOBcxXD3rMYBYuk6FNH3E=
|
||||
github.com/go-llsqlite/crawshaw v0.5.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
@@ -211,12 +210,15 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
@@ -227,8 +229,8 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@@ -236,12 +238,11 @@ github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbc
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
@@ -257,12 +258,11 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lispad/go-generics-tools v1.1.0 h1:mbSgcxdFVmpoyso1X/MJHXbSbSL3dD+qhRryyxk+/XY=
|
||||
github.com/lispad/go-generics-tools v1.1.0/go.mod h1:2csd1EJljo/gy5qG4khXol7ivCPptNjG5Uv2X8MgK84=
|
||||
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
|
||||
@@ -271,8 +271,9 @@ github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlW
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -281,7 +282,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
@@ -298,49 +298,54 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||
github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
|
||||
github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
|
||||
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
||||
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
||||
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts=
|
||||
github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s=
|
||||
github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||
github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
|
||||
github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
|
||||
github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
||||
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ=
|
||||
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||
github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
|
||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||
github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
|
||||
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
|
||||
github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA=
|
||||
github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
|
||||
github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
|
||||
github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
|
||||
github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
|
||||
github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -367,12 +372,14 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
|
||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
@@ -404,9 +411,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
|
||||
github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
||||
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
@@ -416,42 +425,52 @@ github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPyS
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6 h1:NkHVT1GBVV9hUj1iWglFxAuNlvLHELFn71U2H7aOIog=
|
||||
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6/go.mod h1:tDI2vlr15+D9cyN3odhFrZJi3oJj8HlHaOEFPrUiGSY=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
|
||||
go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
|
||||
go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
|
||||
go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -465,26 +484,24 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -494,10 +511,11 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -521,32 +539,38 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -554,14 +578,17 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -570,9 +597,11 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -617,3 +646,13 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/libc v1.40.6 h1:141JHq3SjhOOCjECBgD4K8VgTFOy19CnHwroC08DAig=
|
||||
modernc.org/libc v1.40.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
zombiezen.com/go/sqlite v1.1.0 h1:2BIABTFCE221++xR90rV0mE6T5gFF8o0MUcKDNUrIVc=
|
||||
zombiezen.com/go/sqlite v1.1.0/go.mod h1:rZEFEJSoeXeiwuAb6HugOJLhtDabl+EekT92DCEKOb0=
|
||||
@@ -1,12 +1,14 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
GetConfig func(v any) (bool, error)
|
||||
GetConfig func(v any) bool
|
||||
ProxyUrl *url.URL
|
||||
FileController
|
||||
//ContextDialer() (proxy.Dialer, error)
|
||||
}
|
||||
@@ -21,8 +23,8 @@ type DefaultFileController struct {
|
||||
func NewController() *Controller {
|
||||
return &Controller{
|
||||
FileController: &DefaultFileController{},
|
||||
GetConfig: func(v any) (bool, error) {
|
||||
return false, nil
|
||||
GetConfig: func(v any) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ type Fetcher interface {
|
||||
Pause() error
|
||||
Close() error
|
||||
|
||||
// Stats refreshes health statistics and returns the latest information
|
||||
Stats() any
|
||||
// Meta returns the meta information of the download.
|
||||
Meta() *FetcherMeta
|
||||
// Progress returns the progress of the download.
|
||||
@@ -57,6 +59,15 @@ func (m *FetcherMeta) SingleFilepath() string {
|
||||
return path.Join(m.Opts.Path, file.Path, fileName)
|
||||
}
|
||||
|
||||
// RootDirPath return the root dir path of the task file.
|
||||
func (m *FetcherMeta) RootDirPath() string {
|
||||
if m.Res.Name != "" {
|
||||
return m.FolderPath()
|
||||
} else {
|
||||
return m.Opts.Path
|
||||
}
|
||||
}
|
||||
|
||||
// FetcherBuilder defines the interface for a fetcher builder.
|
||||
type FetcherBuilder interface {
|
||||
// Schemes returns the schemes supported by the fetcher.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package bt
|
||||
|
||||
type config struct {
|
||||
Trackers []string `json:"trackers"`
|
||||
ListenPort int `json:"listenPort"`
|
||||
Trackers []string `json:"trackers"`
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package bt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
@@ -9,7 +10,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -23,12 +26,14 @@ var (
|
||||
)
|
||||
|
||||
type Fetcher struct {
|
||||
ctl *controller.Controller
|
||||
ctl *controller.Controller
|
||||
config *config
|
||||
|
||||
torrent *torrent.Torrent
|
||||
meta *fetcher.FetcherMeta
|
||||
|
||||
torrentReady atomic.Bool
|
||||
torrentDrop atomic.Bool
|
||||
create atomic.Bool
|
||||
progress fetcher.Progress
|
||||
}
|
||||
@@ -42,6 +47,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
exist := f.ctl.GetConfig(&f.config)
|
||||
if !exist {
|
||||
f.config = &config{
|
||||
ListenPort: 0,
|
||||
Trackers: []string{},
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,7 +66,12 @@ func (f *Fetcher) initClient() (err error) {
|
||||
}
|
||||
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
cfg.ListenPort = 0
|
||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||
cfg.ListenPort = f.config.ListenPort
|
||||
if f.ctl.ProxyUrl != nil {
|
||||
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||
}
|
||||
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||
ClientBaseDir: cfg.DataDir,
|
||||
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||
@@ -116,6 +133,7 @@ func (f *Fetcher) Start() (err error) {
|
||||
file.Download()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -126,6 +144,7 @@ func (f *Fetcher) Pause() (err error) {
|
||||
}
|
||||
|
||||
func (f *Fetcher) Close() (err error) {
|
||||
f.torrentDrop.Store(false)
|
||||
f.safeDrop()
|
||||
return nil
|
||||
}
|
||||
@@ -141,6 +160,9 @@ func (f *Fetcher) safeDrop() {
|
||||
|
||||
func (f *Fetcher) Wait() (err error) {
|
||||
for {
|
||||
if f.torrentDrop.Load() {
|
||||
break
|
||||
}
|
||||
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
||||
done := true
|
||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||
@@ -176,6 +198,16 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||
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 {
|
||||
if !f.torrentReady.Load() {
|
||||
return f.progress
|
||||
@@ -195,6 +227,7 @@ func (f *Fetcher) updateRes() {
|
||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||
Hash: f.torrent.InfoHash().String(),
|
||||
}
|
||||
f.torrent.PeerConns()
|
||||
for i, file := range f.torrent.Files() {
|
||||
res.Files[i] = &base.FileInfo{
|
||||
Name: filepath.Base(file.DisplayPath()),
|
||||
@@ -231,11 +264,6 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var cfg config
|
||||
exist, err := f.ctl.GetConfig(&cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// use map to deduplicate
|
||||
trackers := make(map[string]bool)
|
||||
@@ -247,8 +275,8 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if exist && len(cfg.Trackers) > 0 {
|
||||
for _, tracker := range cfg.Trackers {
|
||||
if len(f.config.Trackers) > 0 {
|
||||
for _, tracker := range f.config.Trackers {
|
||||
trackers[tracker] = true
|
||||
}
|
||||
}
|
||||
@@ -288,3 +316,12 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
||||
func parseBep20() string {
|
||||
s := strings.ReplaceAll(base.Version, ".", "")
|
||||
if len(s) < 4 {
|
||||
s += strings.Repeat("0", 4-len(s))
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -50,7 +52,15 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFetcher_Config(t *testing.T) {
|
||||
doResolve(t, buildConfigFetcher())
|
||||
doResolve(t, buildConfigFetcher(nil))
|
||||
}
|
||||
|
||||
func TestFetcher_ResolveWithProxy(t *testing.T) {
|
||||
usr, pwd := "admin", "123"
|
||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||
defer proxyListener.Close()
|
||||
|
||||
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
|
||||
}
|
||||
|
||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
@@ -90,7 +100,7 @@ func buildFetcher() fetcher.Fetcher {
|
||||
return fetcher
|
||||
}
|
||||
|
||||
func buildConfigFetcher() fetcher.Fetcher {
|
||||
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
newController := controller.NewController()
|
||||
mockCfg := config{
|
||||
@@ -98,12 +108,13 @@ func buildConfigFetcher() fetcher.Fetcher {
|
||||
"udp://tracker.birkenwald.de:6969/announce",
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
}}
|
||||
newController.GetConfig = func(v any) (bool, error) {
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||
return false, err
|
||||
return false
|
||||
}
|
||||
return true, nil
|
||||
return true
|
||||
}
|
||||
newController.ProxyUrl = proxyUrl
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package http
|
||||
|
||||
type config struct {
|
||||
Connections int `json:"connections"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Connections int `json:"connections"`
|
||||
UseServerCtime bool `json:"useServerCtime"`
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/xiaoqidun/setft"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"io"
|
||||
"mime"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -51,6 +53,7 @@ func newChunk(begin int64, end int64) *chunk {
|
||||
|
||||
type Fetcher struct {
|
||||
ctl *controller.Controller
|
||||
config *config
|
||||
doneCh chan error
|
||||
|
||||
meta *fetcher.FetcherMeta
|
||||
@@ -71,6 +74,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||
if f.meta == nil {
|
||||
f.meta = &fetcher.FetcherMeta{}
|
||||
}
|
||||
exist := f.ctl.GetConfig(&f.config)
|
||||
if !exist {
|
||||
f.config = &config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
Connections: 1,
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,11 +88,11 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq, err := buildRequest(nil, req)
|
||||
httpReq, err := f.buildRequest(nil, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := buildClient()
|
||||
client := f.buildClient()
|
||||
// send Range request to check whether the server supports breakpoint continuation
|
||||
// just test one byte, Range: bytes=0-0
|
||||
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
||||
@@ -96,8 +106,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
Range: false,
|
||||
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
|
||||
// 解析资源大小: bytes 0-1000/1001 => 1001
|
||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||
@@ -121,8 +132,17 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
} else {
|
||||
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{
|
||||
Size: res.Size,
|
||||
Size: res.Size,
|
||||
Ctime: lastModifiedTime,
|
||||
}
|
||||
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
||||
if contentDisposition != "" {
|
||||
@@ -160,16 +180,7 @@ func (f *Fetcher) Create(opts *base.Options) error {
|
||||
}
|
||||
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||
if extra.Connections == 0 {
|
||||
var cfg config
|
||||
exist, err := f.ctl.GetConfig(&cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
extra.Connections = cfg.Connections
|
||||
} else {
|
||||
extra.Connections = 1
|
||||
}
|
||||
extra.Connections = f.config.Connections
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -218,6 +229,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||
return f.meta
|
||||
}
|
||||
|
||||
func (f *Fetcher) Stats() any {
|
||||
// todo http download health
|
||||
return &fhttp.Stats{}
|
||||
}
|
||||
|
||||
func (f *Fetcher) Progress() fetcher.Progress {
|
||||
p := make(fetcher.Progress, 0)
|
||||
if len(f.chunks) > 0 {
|
||||
@@ -252,6 +268,10 @@ func (f *Fetcher) fetch() {
|
||||
return
|
||||
}
|
||||
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
|
||||
}()
|
||||
}
|
||||
@@ -260,17 +280,17 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
chunk := f.chunks[index]
|
||||
chunk.retryTimes = 0
|
||||
|
||||
httpReq, err := buildRequest(ctx, f.meta.Req)
|
||||
httpReq, err := f.buildRequest(ctx, f.meta.Req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
client = buildClient()
|
||||
client = f.buildClient()
|
||||
buf = make([]byte, 8192)
|
||||
maxRetries = 3
|
||||
)
|
||||
// retry until all remain chunks failed
|
||||
for true {
|
||||
for {
|
||||
// if chunk is completed, return
|
||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||
return
|
||||
@@ -307,26 +327,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||
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
|
||||
}
|
||||
// Http request success, reset retry times
|
||||
chunk.retryTimes = 0
|
||||
err = func() error {
|
||||
defer resp.Body.Close()
|
||||
// Http request success, reset retry times
|
||||
chunk.retryTimes = 0
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
@@ -343,7 +350,18 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
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 err == nil {
|
||||
for _, c := range f.chunks {
|
||||
@@ -357,6 +375,52 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
||||
url, err := url.Parse(req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
method string
|
||||
body io.Reader
|
||||
)
|
||||
headers := make(map[string][]string)
|
||||
if req.Extra == nil {
|
||||
method = http.MethodGet
|
||||
} else {
|
||||
extra := req.Extra.(*fhttp.ReqExtra)
|
||||
if extra.Method != "" {
|
||||
method = extra.Method
|
||||
} else {
|
||||
method = http.MethodGet
|
||||
}
|
||||
if len(extra.Header) > 0 {
|
||||
for k, v := range extra.Header {
|
||||
headers[k] = []string{v}
|
||||
}
|
||||
}
|
||||
if extra.Body != "" {
|
||||
body = bytes.NewBufferString(extra.Body)
|
||||
}
|
||||
}
|
||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||
// load user agent from config
|
||||
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
||||
} else {
|
||||
httpReq, err = http.NewRequest(method, url.String(), body)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header = headers
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
if f.meta.Res.Range {
|
||||
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
||||
@@ -385,59 +449,19 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||
return
|
||||
}
|
||||
|
||||
func buildClient() *http.Client {
|
||||
func (f *Fetcher) buildClient() *http.Client {
|
||||
transport := &http.Transport{}
|
||||
// Cookie handle
|
||||
jar, _ := cookiejar.New(nil)
|
||||
if f.ctl.ProxyUrl != nil {
|
||||
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||
}
|
||||
return &http.Client{
|
||||
Jar: jar,
|
||||
Transport: transport,
|
||||
Jar: jar,
|
||||
}
|
||||
}
|
||||
|
||||
func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
||||
url, err := url.Parse(req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
method string
|
||||
body io.Reader
|
||||
)
|
||||
headers := make(map[string][]string)
|
||||
if req.Extra == nil {
|
||||
method = http.MethodGet
|
||||
} else {
|
||||
extra := req.Extra.(*fhttp.ReqExtra)
|
||||
if extra.Method != "" {
|
||||
method = extra.Method
|
||||
} else {
|
||||
method = http.MethodGet
|
||||
}
|
||||
if len(extra.Header) > 0 {
|
||||
for k, v := range extra.Header {
|
||||
headers[k] = []string{v}
|
||||
}
|
||||
}
|
||||
if extra.Body != "" {
|
||||
body = bytes.NewBufferString(extra.Body)
|
||||
}
|
||||
}
|
||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||
headers[base.HttpHeaderUserAgent] = []string{base.AgentName}
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
||||
} else {
|
||||
httpReq, err = http.NewRequest(method, url.String(), body)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header = headers
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
type fetcherData struct {
|
||||
Chunks []*chunk
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -68,7 +68,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func TestFetcher_DownloadError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadLimit(t *testing.T) {
|
||||
listener := test.StartTestLimitServer()
|
||||
listener := test.StartTestLimitServer(4, 0)
|
||||
defer listener.Close()
|
||||
|
||||
downloadNormal(listener, 1, t)
|
||||
@@ -141,10 +141,43 @@ func TestFetcher_DownloadResume(t *testing.T) {
|
||||
downloadResume(listener, 16, t)
|
||||
}
|
||||
|
||||
func TestFetcher_Config(t *testing.T) {
|
||||
func TestFetcher_DownloadWithProxy(t *testing.T) {
|
||||
httpListener := test.StartTestFileServer()
|
||||
defer httpListener.Close()
|
||||
proxyListener := test.StartSocks5Server("", "")
|
||||
defer proxyListener.Close()
|
||||
|
||||
downloadWithProxy(httpListener, proxyListener, t)
|
||||
}
|
||||
|
||||
func TestFetcher_ConfigConnections(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
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()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -302,23 +335,40 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||
fetcher := downloadReady(httpListener, 4, t)
|
||||
ctl := controller.NewController()
|
||||
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
|
||||
fetcher.Setup(ctl)
|
||||
err := fetcher.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = fetcher.Wait()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := test.FileMd5(test.BuildFile)
|
||||
got := test.FileMd5(test.DownloadFile)
|
||||
if want != got {
|
||||
t.Errorf("Download() got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func buildFetcher() *Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
fetcher.Setup(controller.NewController())
|
||||
return fetcher.(*Fetcher)
|
||||
}
|
||||
|
||||
func buildConfigFetcher() fetcher.Fetcher {
|
||||
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||
fetcher := new(FetcherBuilder).Build()
|
||||
newController := controller.NewController()
|
||||
mockCfg := config{
|
||||
Connections: 16,
|
||||
}
|
||||
newController.GetConfig = func(v any) (bool, error) {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||
return false, err
|
||||
newController.GetConfig = func(v any) bool {
|
||||
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
||||
return false
|
||||
}
|
||||
return true, nil
|
||||
return true
|
||||
}
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/armon/go-socks5"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -138,7 +141,7 @@ func StartTestErrorServer() net.Listener {
|
||||
}
|
||||
|
||||
// StartTestLimitServer connections limit server
|
||||
func StartTestLimitServer() net.Listener {
|
||||
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||
var connections atomic.Int32
|
||||
|
||||
return startTestServer(func() http.Handler {
|
||||
@@ -148,7 +151,7 @@ func StartTestLimitServer() net.Listener {
|
||||
connections.Add(-1)
|
||||
}()
|
||||
connections.Add(1)
|
||||
if connections.Load() > 4 {
|
||||
if maxConnections != 0 && connections.Load() > maxConnections {
|
||||
writer.WriteHeader(403)
|
||||
return
|
||||
}
|
||||
@@ -162,7 +165,7 @@ func StartTestLimitServer() net.Listener {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
io.Copy(writer, file)
|
||||
slowCopy(writer, file, delay)
|
||||
} else {
|
||||
// split range
|
||||
s := strings.Split(r, "=")
|
||||
@@ -203,13 +206,57 @@ func StartTestLimitServer() net.Listener {
|
||||
}
|
||||
defer file.Close()
|
||||
file.Seek(start, 0)
|
||||
io.CopyN(writer, file, end-start+1)
|
||||
slowCopyN(writer, file, end-start+1, delay)
|
||||
}
|
||||
})
|
||||
return mux
|
||||
})
|
||||
}
|
||||
|
||||
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
|
||||
func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write(buf[0:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
if ew != nil {
|
||||
err = ew
|
||||
break
|
||||
}
|
||||
if nr != nw {
|
||||
err = io.ErrShortWrite
|
||||
break
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
if er != io.EOF {
|
||||
err = er
|
||||
}
|
||||
break
|
||||
}
|
||||
if delay > 0 {
|
||||
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||
}
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
|
||||
if written == n {
|
||||
return n, nil
|
||||
}
|
||||
if written < n && err == nil {
|
||||
// src stopped early; must have been EOF.
|
||||
err = io.EOF
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -271,3 +318,32 @@ func ifExistAndRemove(name string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartSocks5Server(usr, pwd string) net.Listener {
|
||||
conf := &socks5.Config{}
|
||||
if usr != "" && pwd != "" {
|
||||
conf.Credentials = socks5.StaticCredentials{
|
||||
usr: pwd,
|
||||
}
|
||||
}
|
||||
|
||||
server, err := socks5.New(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0") // 你可以根据需要更改监听地址
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go server.Serve(listener)
|
||||
return listener
|
||||
}
|
||||
|
||||
func AssertResourceEqual(want, got *base.Resource) bool {
|
||||
// Ignore ctime
|
||||
if got != nil && len(got.Files) > 0 {
|
||||
got.Files[0].Ctime = nil
|
||||
}
|
||||
return reflect.DeepEqual(want, got)
|
||||
}
|
||||
@@ -16,14 +16,13 @@ const (
|
||||
HttpCodePartialContent = 206
|
||||
|
||||
HttpHeaderRange = "Range"
|
||||
HttpHeaderAcceptRanges = "Accept-Ranges"
|
||||
HttpHeaderContentLength = "Content-Length"
|
||||
HttpHeaderContentRange = "Content-Range"
|
||||
HttpHeaderContentDisposition = "Content-Disposition"
|
||||
HttpHeaderUserAgent = "User-Agent"
|
||||
HttpHeaderLastModified = "Last-Modified"
|
||||
|
||||
HttpHeaderBytes = "bytes"
|
||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||
)
|
||||
|
||||
const (
|
||||
AgentName = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
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
|
||||
var InDocker string
|
||||
|
||||
func init() {
|
||||
if Version == "" {
|
||||
Version = "dev"
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"golang.org/x/exp/slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Request download request
|
||||
@@ -59,9 +60,10 @@ func (r *Resource) CalcSize(selectFiles []int) {
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
Ctime *time.Time `json:"ctime"`
|
||||
|
||||
Req *Request `json:"req"`
|
||||
}
|
||||
|
||||
+234
-114
@@ -6,8 +6,11 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
"github.com/virtuald/go-paniclog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -32,7 +35,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTaskNotFound = errors.New("task not found")
|
||||
ErrTaskNotFound = errors.New("task not found")
|
||||
ErrUnSupportedProtocol = errors.New("unsupported protocol")
|
||||
)
|
||||
|
||||
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.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
|
||||
if cfg.ProductionMode {
|
||||
@@ -119,6 +124,11 @@ func (d *Downloader) Setup() error {
|
||||
}
|
||||
// init default config
|
||||
d.cfg.DownloaderStoreConfig.Init()
|
||||
for _, fb := range d.fetcherBuilders {
|
||||
f := fb.Build()
|
||||
d.setupFetcher(f)
|
||||
|
||||
}
|
||||
// load tasks from storage
|
||||
var tasks []*Task
|
||||
if err = d.storage.List(bucketTask, &tasks); err != nil {
|
||||
@@ -162,8 +172,8 @@ func (d *Downloader) Setup() error {
|
||||
if len(d.tasks) > 0 {
|
||||
for _, task := range d.tasks {
|
||||
func() {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
task.statusLock.Lock()
|
||||
defer task.statusLock.Unlock()
|
||||
if task.Status != base.DownloadStatusRunning {
|
||||
return
|
||||
}
|
||||
@@ -207,14 +217,15 @@ func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
|
||||
if ok {
|
||||
return fetchBuilder, nil
|
||||
}
|
||||
return nil, errors.New("unsupported protocol")
|
||||
return nil, ErrUnSupportedProtocol
|
||||
}
|
||||
|
||||
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
|
||||
ctl := controller.NewController()
|
||||
ctl.GetConfig = func(v any) (bool, error) {
|
||||
ctl.GetConfig = func(v any) bool {
|
||||
return d.getProtocolConfig(fetcher.Name(), v)
|
||||
}
|
||||
ctl.ProxyUrl = d.cfg.ProxyUrl()
|
||||
fetcher.Setup(ctl)
|
||||
}
|
||||
|
||||
@@ -224,7 +235,10 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
res := d.triggerOnResolve(req)
|
||||
res, err := d.triggerOnResolve(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if res != nil && len(res.Files) > 0 {
|
||||
rr = &ResolveResult{
|
||||
Res: res,
|
||||
@@ -288,9 +302,6 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
|
||||
}
|
||||
|
||||
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
|
||||
if opts == nil {
|
||||
opts = &base.Options{}
|
||||
}
|
||||
d.fetcherMapLock.RLock()
|
||||
fetcher, ok := d.fetcherCache[rrId]
|
||||
d.fetcherMapLock.RUnlock()
|
||||
@@ -318,8 +329,7 @@ func (d *Downloader) Pause(id string) (err error) {
|
||||
if task.Status == base.DownloadStatusPause {
|
||||
return
|
||||
}
|
||||
err = d.doPause(task)
|
||||
if err != nil {
|
||||
if err = d.doPause(task); err != nil {
|
||||
return
|
||||
}
|
||||
d.notifyRunning()
|
||||
@@ -352,19 +362,7 @@ func (d *Downloader) Continue(id string) (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
func() {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
return
|
||||
}
|
||||
|
||||
if err = d.doStart(task); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
return
|
||||
return d.doStart(task)
|
||||
}
|
||||
|
||||
func (d *Downloader) PauseAll() (err error) {
|
||||
@@ -377,13 +375,7 @@ func (d *Downloader) PauseAll() (err error) {
|
||||
}()
|
||||
|
||||
for _, task := range d.tasks {
|
||||
err = func() error {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
return d.doPause(task)
|
||||
}()
|
||||
if err != nil {
|
||||
if err = d.doPause(task); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -413,13 +405,7 @@ func (d *Downloader) ContinueAll() (err error) {
|
||||
|
||||
for _, task := range continuedTasks {
|
||||
tt := task
|
||||
err = func() error {
|
||||
tt.lock.Lock()
|
||||
defer tt.lock.Unlock()
|
||||
|
||||
return d.doStart(tt)
|
||||
}()
|
||||
if err != nil {
|
||||
if err = d.doStart(tt); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -440,6 +426,13 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -452,19 +445,73 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
|
||||
err = func() error {
|
||||
if task.fetcher != nil {
|
||||
if err := task.fetcher.Close(); err != nil {
|
||||
return err
|
||||
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) {
|
||||
err = func() error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if err := d.storage.Delete(bucketTask, task.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.storage.Delete(bucketSave, task.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if task.fetcher != nil {
|
||||
if err := task.fetcher.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if force && task.Meta.Res != nil {
|
||||
if task.Meta.Res.Name != "" {
|
||||
if err := os.RemoveAll(task.Meta.FolderPath()); err != nil {
|
||||
@@ -524,9 +571,6 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
|
||||
Task: task,
|
||||
Err: err,
|
||||
})
|
||||
if eventKey == EventKeyError {
|
||||
d.emit(EventKeyFinally, task, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,17 +584,22 @@ func (d *Downloader) GetTask(id string) *Task {
|
||||
}
|
||||
|
||||
func (d *Downloader) GetTasks() []*Task {
|
||||
tasks := make([]*Task, len(d.tasks))
|
||||
for i := 0; i < len(d.tasks); i++ {
|
||||
tasks[i] = d.tasks[i]
|
||||
return d.tasks
|
||||
}
|
||||
|
||||
func (d *Downloader) GetTasksByStatues(statues []base.Status) []*Task {
|
||||
if len(statues) == 0 {
|
||||
return d.tasks
|
||||
}
|
||||
// sort tasks by status, order by Status(if(running,1,0)) desc, CreatedAt desc
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
if tasks[i].Status != tasks[j].Status {
|
||||
return tasks[i].Status == base.DownloadStatusRunning
|
||||
tasks := make([]*Task, 0)
|
||||
for _, task := range d.tasks {
|
||||
for _, status := range statues {
|
||||
if task.Status == status {
|
||||
tasks = append(tasks, task)
|
||||
break
|
||||
}
|
||||
}
|
||||
return tasks[i].CreatedAt.After(tasks[j].CreatedAt)
|
||||
})
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
@@ -563,44 +612,75 @@ func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
|
||||
return d.storage.Put(bucketConfig, "config", v)
|
||||
}
|
||||
|
||||
func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) {
|
||||
func (d *Downloader) getProtocolConfig(name string, v any) bool {
|
||||
cfg, err := d.GetConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false
|
||||
}
|
||||
if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil {
|
||||
return false, nil
|
||||
return false
|
||||
}
|
||||
if err := util.MapToStruct(cfg.ProtocolConfig[name], v); err != nil {
|
||||
return false, err
|
||||
d.Logger.Warn().Err(err).Msgf("get protocol config failed")
|
||||
return false
|
||||
}
|
||||
return true, nil
|
||||
return true
|
||||
}
|
||||
|
||||
// wait task done
|
||||
func (d *Downloader) watch(task *Task) {
|
||||
err := task.fetcher.Wait()
|
||||
if err != nil {
|
||||
task.Status = base.DownloadStatusError
|
||||
d.emit(EventKeyError, task, err)
|
||||
} 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)
|
||||
d.doOnError(task, err)
|
||||
return
|
||||
}
|
||||
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()
|
||||
|
||||
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 {
|
||||
@@ -635,6 +715,8 @@ func (d *Downloader) restoreFetcher(task *Task) error {
|
||||
task.fetcher.Meta().Res = task.Meta.Res
|
||||
}
|
||||
go d.watch(task)
|
||||
} else if task.Status == base.DownloadStatusError {
|
||||
go d.watch(task)
|
||||
}
|
||||
task.fetcher.Create(task.Meta.Opts)
|
||||
return nil
|
||||
@@ -644,6 +726,9 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
if opts == nil {
|
||||
opts = &base.Options{}
|
||||
}
|
||||
if opts.SelectFiles == nil {
|
||||
opts.SelectFiles = make([]int, 0)
|
||||
}
|
||||
|
||||
meta := fetcher.Meta()
|
||||
meta.Opts = opts
|
||||
@@ -663,9 +748,9 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
task := NewTask()
|
||||
task.fetcherBuilder = fb
|
||||
task.fetcher = fetcher
|
||||
task.Protocol = fetcher.Name()
|
||||
task.Meta = fetcher.Meta()
|
||||
task.Progress = &Progress{}
|
||||
task.Status = base.DownloadStatusReady
|
||||
initTask(task)
|
||||
if err = fetcher.Create(opts); err != nil {
|
||||
return
|
||||
@@ -688,41 +773,55 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
return
|
||||
}
|
||||
|
||||
d.doStart(task)
|
||||
err = d.doStart(task)
|
||||
}()
|
||||
|
||||
go d.watch(task)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) statusMut(task *Task, fn func() (bool, error)) (bool, error) {
|
||||
task.statusLock.Lock()
|
||||
defer task.statusLock.Unlock()
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
func (d *Downloader) doStart(task *Task) (err error) {
|
||||
if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone {
|
||||
err := d.restoreFetcher(task)
|
||||
var isCreate bool
|
||||
isReturn, err := d.statusMut(task, func() (isReturn bool, err error) {
|
||||
if task.Status == base.DownloadStatusRunning || task.Status == base.DownloadStatusDone {
|
||||
isReturn = true
|
||||
return
|
||||
}
|
||||
|
||||
err = d.restoreFetcher(task)
|
||||
if err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
||||
return err
|
||||
return
|
||||
}
|
||||
isCreate = task.Status == base.DownloadStatusReady
|
||||
|
||||
d.triggerOnStart(task)
|
||||
task.updateStatus(base.DownloadStatusRunning)
|
||||
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID)
|
||||
return
|
||||
}
|
||||
if isReturn {
|
||||
return
|
||||
}
|
||||
|
||||
cloneTask := task.clone()
|
||||
isCreate := task.Status == base.DownloadStatusReady
|
||||
task.Status = base.DownloadStatusRunning
|
||||
|
||||
doStart := func() error {
|
||||
handler := func() error {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
req := d.triggerOnStart(cloneTask)
|
||||
if req != nil {
|
||||
task.Meta.Req = req
|
||||
task.fetcher.Meta().Req = req
|
||||
}
|
||||
|
||||
if task.Meta.Res == nil {
|
||||
err := task.fetcher.Resolve(task.Meta.Req)
|
||||
if err != nil {
|
||||
task.Status = base.DownloadStatusError
|
||||
d.emit(EventKeyError, task, err)
|
||||
return err
|
||||
}
|
||||
task.Meta.Res = task.fetcher.Meta().Res
|
||||
@@ -731,8 +830,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
if isCreate {
|
||||
d.checkDuplicateLock.Lock()
|
||||
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.
|
||||
if task.Meta.Res.Name != "" {
|
||||
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
|
||||
fullDirPath := task.Meta.FolderPath()
|
||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||
if err != nil {
|
||||
@@ -740,6 +841,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
}
|
||||
task.Meta.Opts.Name = newName
|
||||
} else {
|
||||
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
|
||||
fullFilePath := task.Meta.SingleFilepath()
|
||||
newName, err := util.CheckDuplicateAndRename(fullFilePath)
|
||||
if err != nil {
|
||||
@@ -763,9 +865,8 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
err := doStart()
|
||||
if err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID)
|
||||
if err := handler(); err != nil {
|
||||
d.doOnError(task, err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -773,27 +874,44 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
||||
}
|
||||
|
||||
func (d *Downloader) doPause(task *Task) (err error) {
|
||||
err = func() error {
|
||||
if task.Status != base.DownloadStatusDone {
|
||||
task.Status = base.DownloadStatusPause
|
||||
task.timer.Pause()
|
||||
if task.fetcher != nil {
|
||||
if err := task.fetcher.Pause(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
return err
|
||||
}
|
||||
d.emit(EventKeyPause, task)
|
||||
isReturn, err := d.statusMut(task, func() (isReturn bool, err error) {
|
||||
if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusDone {
|
||||
isReturn = true
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
task.updateStatus(base.DownloadStatusPause)
|
||||
task.timer.Pause()
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("pause task failed, task id: %s", task.ID)
|
||||
return
|
||||
}
|
||||
if isReturn {
|
||||
return
|
||||
}
|
||||
|
||||
handler := func() error {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
if task.fetcher != nil {
|
||||
if err := task.fetcher.Pause(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
return err
|
||||
}
|
||||
d.emit(EventKeyPause, task)
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
if err := handler(); err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("pause task handle failed, task id: %s", task.ID)
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -821,6 +939,8 @@ func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||
|
||||
func initTask(task *Task) {
|
||||
task.timer = util.NewTimer(task.Progress.Used)
|
||||
|
||||
task.statusLock = &sync.Mutex{}
|
||||
task.lock = &sync.Mutex{}
|
||||
task.speedArr = make([]int64, 0)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
@@ -84,6 +83,82 @@ func TestDownloader_Create(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloader_CreateWithProxy(t *testing.T) {
|
||||
// No proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return nil
|
||||
})
|
||||
// Disable proxy
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Enable = false
|
||||
return proxyCfg
|
||||
})
|
||||
// Invalid proxy scheme
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Scheme = ""
|
||||
return proxyCfg
|
||||
})
|
||||
// Invalid proxy host
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
proxyCfg.Host = ""
|
||||
return proxyCfg
|
||||
})
|
||||
// Use proxy without auth
|
||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
})
|
||||
// Use proxy with auth
|
||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
||||
return proxyCfg
|
||||
})
|
||||
}
|
||||
|
||||
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig) {
|
||||
httpListener := test.StartTestFileServer()
|
||||
defer httpListener.Close()
|
||||
usr, pwd := "", ""
|
||||
if auth {
|
||||
usr, pwd = "admin", "123"
|
||||
}
|
||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||
defer proxyListener.Close()
|
||||
|
||||
downloader := NewDownloader(nil)
|
||||
if err := downloader.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer downloader.Clear()
|
||||
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
Scheme: "socks5",
|
||||
Host: proxyListener.Addr().String(),
|
||||
Usr: usr,
|
||||
Pwd: pwd,
|
||||
})
|
||||
|
||||
req := &base.Request{
|
||||
URL: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
|
||||
}
|
||||
rr, err := downloader.Resolve(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := &base.Resource{
|
||||
Size: test.BuildSize,
|
||||
Range: true,
|
||||
Files: []*base.FileInfo{
|
||||
{
|
||||
Name: test.BuildName,
|
||||
Path: "",
|
||||
Size: test.BuildSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
if !test.AssertResourceEqual(want, rr.Res) {
|
||||
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloader_CreateRename(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
defer listener.Close()
|
||||
@@ -205,10 +280,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
||||
defer downloader.Clear()
|
||||
|
||||
var httpCfg map[string]any
|
||||
exits, err := downloader.getProtocolConfig("http", &httpCfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exits := downloader.getProtocolConfig("http", &httpCfg)
|
||||
if exits {
|
||||
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
|
||||
}
|
||||
@@ -234,7 +306,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if err = downloader.PutConfig(storeCfg); err != nil {
|
||||
if err := downloader.PutConfig(storeCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,16 @@ package engine
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
"github.com/dop251/goja_nodejs/process"
|
||||
"github.com/dop251/goja_nodejs/url"
|
||||
gojaurl "github.com/dop251/goja_nodejs/url"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed polyfill/out/index.js
|
||||
@@ -27,12 +29,16 @@ type Engine struct {
|
||||
func (e *Engine) RunString(script string) (value any, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
var result goja.Value
|
||||
e.loop.Run(func(runtime *goja.Runtime) {
|
||||
result, err = runtime.RunString(script)
|
||||
if err == nil {
|
||||
go e.await(result)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
@@ -45,9 +51,10 @@ func (e *Engine) RunString(script string) (value any, err error) {
|
||||
func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
var result goja.Value
|
||||
e.loop.Run(func(runtime *goja.Runtime) {
|
||||
if args == nil {
|
||||
@@ -59,18 +66,57 @@ func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err err
|
||||
}
|
||||
result, err = fn(nil, jsArgs...)
|
||||
}
|
||||
if err == nil {
|
||||
go e.await(result)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
return resolveResult(result)
|
||||
}
|
||||
|
||||
// loop.Run will hang if the script result has a non-stop code, such as setInterval.
|
||||
// This method will stop the event loop when the promise result is resolved.
|
||||
func (e *Engine) await(value any) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if v, ok := value.(goja.Value); ok {
|
||||
// if result is promise, wait for it to be resolved
|
||||
if p, ok := v.Export().(*goja.Promise); ok {
|
||||
if p.State() != goja.PromiseStatePending {
|
||||
return
|
||||
}
|
||||
|
||||
// check promise state every 100 milliseconds, until it is resolved
|
||||
for {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
if p.State() == goja.PromiseStatePending {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// stop the event loop
|
||||
e.loop.StopNoWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) Close() {
|
||||
e.loop.Stop()
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
type Config struct {
|
||||
ProxyURL *url.URL
|
||||
}
|
||||
|
||||
func NewEngine(cfg *Config) *Engine {
|
||||
if cfg == nil {
|
||||
cfg = &Config{}
|
||||
}
|
||||
loop := eventloop.NewEventLoop()
|
||||
engine := &Engine{
|
||||
loop: loop,
|
||||
@@ -78,15 +124,18 @@ func NewEngine() *Engine {
|
||||
loop.Run(func(runtime *goja.Runtime) {
|
||||
engine.Runtime = runtime
|
||||
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||
url.Enable(runtime)
|
||||
process.Enable(runtime)
|
||||
vm.Enable(runtime)
|
||||
gojaurl.Enable(runtime)
|
||||
if err := gojaerror.Enable(runtime); err != nil {
|
||||
return
|
||||
}
|
||||
if err := file.Enable(runtime); err != nil {
|
||||
return
|
||||
}
|
||||
if err := formdata.Enable(runtime); err != nil {
|
||||
return
|
||||
}
|
||||
if err := xhr.Enable(runtime); err != nil {
|
||||
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := runtime.RunString(polyfillScript); err != nil {
|
||||
@@ -96,13 +145,17 @@ func NewEngine() *Engine {
|
||||
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
||||
return
|
||||
}
|
||||
// polyfill window.location
|
||||
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
})
|
||||
return engine
|
||||
}
|
||||
|
||||
func Run(script string) (value any, err error) {
|
||||
engine := NewEngine()
|
||||
engine := NewEngine(nil)
|
||||
return engine.RunString(script)
|
||||
}
|
||||
|
||||
@@ -118,7 +171,11 @@ func resolveResult(value goja.Value) (any, error) {
|
||||
case goja.PromiseStateFulfilled:
|
||||
return p.Result().Export(), nil
|
||||
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
|
||||
|
||||
@@ -7,7 +7,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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"
|
||||
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/dop251/goja"
|
||||
"io"
|
||||
"net"
|
||||
@@ -19,16 +22,28 @@ import (
|
||||
)
|
||||
|
||||
func TestPolyfill(t *testing.T) {
|
||||
doTestPolyfill(t, "MessageError")
|
||||
doTestPolyfill(t, "XMLHttpRequest")
|
||||
doTestPolyfill(t, "Blob")
|
||||
doTestPolyfill(t, "FormData")
|
||||
doTestPolyfill(t, "fetch")
|
||||
doTestPolyfill(t, "__gopeed_create_vm")
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
engine := NewEngine(nil)
|
||||
_, err := engine.RunString(`
|
||||
throw new MessageError('test');
|
||||
`)
|
||||
if me, ok := gojautil.AssertError[*gojaerror.MessageError](err); !ok {
|
||||
t.Fatalf("expect MessageError, but got %v", me)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetch(t *testing.T) {
|
||||
server := startServer()
|
||||
defer server.Close()
|
||||
engine := NewEngine()
|
||||
engine := NewEngine(nil)
|
||||
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", server.Addr().String())); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -191,6 +206,97 @@ function testTimeout(){
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchWithProxy(t *testing.T) {
|
||||
doTestFetchWithProxy(t, "", "")
|
||||
doTestFetchWithProxy(t, "admin", "123")
|
||||
}
|
||||
|
||||
func doTestFetchWithProxy(t *testing.T, usr, pwd string) {
|
||||
httpListener := startServer()
|
||||
defer httpListener.Close()
|
||||
|
||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||
defer proxyListener.Close()
|
||||
|
||||
engine := NewEngine(&Config{ProxyURL: util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)})
|
||||
|
||||
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
respCode, err := engine.RunString(`
|
||||
(async function(){
|
||||
const resp = await fetch(host+'/get');
|
||||
return resp.status;
|
||||
})()
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if respCode != int64(200) {
|
||||
t.Fatalf("fetch with proxy failed, want %d, got %d", 200, respCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVm(t *testing.T) {
|
||||
engine := NewEngine(nil)
|
||||
|
||||
value, err := engine.RunString(`
|
||||
const vm = __gopeed_create_vm()
|
||||
vm.set('a', 1)
|
||||
vm.set('b', 2)
|
||||
const result = vm.runString('a=a+1;b=b+1;a+b;')
|
||||
const out = {
|
||||
"a": vm.get('a'),
|
||||
"b": vm.get('b'),
|
||||
"result": result
|
||||
}
|
||||
out
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]any{
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
"result": 5,
|
||||
}
|
||||
if !test.JsonEqual(value, want) {
|
||||
t.Fatalf("vm test failed, want %v, got %v", want, value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonStopLoop(t *testing.T) {
|
||||
engine := NewEngine(nil)
|
||||
|
||||
_, err := engine.RunString(`
|
||||
function leak(){
|
||||
setInterval(() => {
|
||||
},500)
|
||||
}
|
||||
|
||||
function test(){
|
||||
leak()
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve('done')
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
val, err := callTestFun(engine, "test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if val != "done" {
|
||||
t.Fatalf("infinite loop test failed, want %s, got %s", "done", val)
|
||||
}
|
||||
}
|
||||
|
||||
func doTestPolyfill(t *testing.T, module string) {
|
||||
value, err := Run(fmt.Sprintf(`
|
||||
!!globalThis['%s']
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type MessageError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *MessageError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) error {
|
||||
messageError := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
var message string
|
||||
if len(call.Arguments) > 0 {
|
||||
message = call.Arguments[0].String()
|
||||
}
|
||||
instance := &MessageError{
|
||||
Message: message,
|
||||
}
|
||||
instanceValue := runtime.ToValue(instance).(*goja.Object)
|
||||
instanceValue.SetPrototype(call.This.Prototype())
|
||||
return instanceValue
|
||||
})
|
||||
return runtime.Set("MessageError", messageError)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package inject
|
||||
|
||||
import (
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func ThrowTypeError(vm *goja.Runtime, msg string) {
|
||||
panic(vm.NewTypeError(msg))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"github.com/dop251/goja"
|
||||
"github.com/dop251/goja_nodejs/eventloop"
|
||||
)
|
||||
|
||||
type Vm struct {
|
||||
loop *eventloop.EventLoop
|
||||
}
|
||||
|
||||
func (vm *Vm) Set(name string, value any) {
|
||||
vm.loop.Run(func(runtime *goja.Runtime) {
|
||||
runtime.Set(name, value)
|
||||
})
|
||||
}
|
||||
|
||||
func (vm *Vm) Get(name string) (value any) {
|
||||
vm.loop.Run(func(runtime *goja.Runtime) {
|
||||
value = runtime.Get(name)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (vm *Vm) RunString(script string) (value any, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
vm.loop.Run(func(runtime *goja.Runtime) {
|
||||
value, err = runtime.RunString(script)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) error {
|
||||
return runtime.Set("__gopeed_create_vm", func(call goja.FunctionCall) goja.Value {
|
||||
return runtime.ToValue(&Vm{
|
||||
loop: eventloop.NewEventLoop(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -2,14 +2,15 @@ package xhr
|
||||
|
||||
import (
|
||||
"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/formdata"
|
||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||
"github.com/dop251/goja"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -119,6 +120,7 @@ type XMLHttpRequest struct {
|
||||
requestHeaders map[string]string
|
||||
responseHeaders map[string]string
|
||||
aborted bool
|
||||
proxyUrl *url.URL
|
||||
|
||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||
Timeout int `json:"timeout"`
|
||||
@@ -200,8 +202,13 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
for k, v := range xhr.requestHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
transport := &http.Transport{}
|
||||
if xhr.proxyUrl != nil {
|
||||
transport.Proxy = http.ProxyURL(xhr.proxyUrl)
|
||||
}
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
||||
Transport: transport,
|
||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
@@ -306,10 +313,10 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
|
||||
return data.String()
|
||||
}
|
||||
|
||||
func Enable(runtime *goja.Runtime) error {
|
||||
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
|
||||
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
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{
|
||||
Type: call.Argument(0).String(),
|
||||
@@ -320,6 +327,7 @@ func Enable(runtime *goja.Runtime) error {
|
||||
})
|
||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
instance := &XMLHttpRequest{
|
||||
proxyUrl: proxyUrl,
|
||||
Upload: &XMLHttpRequestUpload{
|
||||
EventProp: &EventProp{
|
||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func ThrowTypeError(vm *goja.Runtime, msg string) {
|
||||
panic(vm.NewTypeError(msg))
|
||||
}
|
||||
|
||||
func AssertError[T error](err error) (t T, r bool) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if e, ok := err.(T); ok {
|
||||
return e, true
|
||||
}
|
||||
if e, ok := err.(*goja.Exception); ok {
|
||||
if ee, okk := e.Value().Export().(T); okk {
|
||||
return ee, true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
+84
-23
@@ -6,9 +6,12 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"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/dop251/goja"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
@@ -34,7 +37,7 @@ type ActivationEvent string
|
||||
const (
|
||||
EventOnResolve ActivationEvent = "onResolve"
|
||||
EventOnStart ActivationEvent = "onStart"
|
||||
//EventOnError ActivationEvent = "onError"
|
||||
EventOnError ActivationEvent = "onError"
|
||||
//EventOnDone ActivationEvent = "onDone"
|
||||
)
|
||||
|
||||
@@ -195,11 +198,19 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
|
||||
if err := util.RmAndMkDirAll(tempExtDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxyOptions := transport.ProxyOptions{}
|
||||
proxyUrl := d.cfg.ProxyUrl()
|
||||
if proxyUrl != nil {
|
||||
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
|
||||
proxyOptions.Username = proxyUrl.User.Username()
|
||||
proxyOptions.Password, _ = proxyUrl.User.Password()
|
||||
}
|
||||
// clone project to extension temp dir
|
||||
gitUrl := parentPath + projectPath + gitSuffix
|
||||
if _, err := git.PlainClone(tempExtDir, false, &git.CloneOptions{
|
||||
URL: gitUrl,
|
||||
Depth: 1,
|
||||
URL: gitUrl,
|
||||
Depth: 1,
|
||||
ProxyOptions: proxyOptions,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -229,8 +240,8 @@ func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) {
|
||||
return &ext, nil
|
||||
}
|
||||
|
||||
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
||||
doTrigger(d,
|
||||
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource, err error) {
|
||||
err = doTrigger(d,
|
||||
EventOnResolve,
|
||||
req,
|
||||
&OnResolveContext{
|
||||
@@ -243,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())
|
||||
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)
|
||||
}
|
||||
res = ctx.Res
|
||||
@@ -251,12 +266,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
|
||||
func (d *Downloader) triggerOnStart(task *Task) {
|
||||
doTrigger(d,
|
||||
EventOnStart,
|
||||
task.Meta.Req,
|
||||
&OnStartContext{
|
||||
Task: task,
|
||||
Task: NewExtensionTask(d, task),
|
||||
},
|
||||
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
|
||||
// Validate request structure
|
||||
@@ -265,14 +280,25 @@ func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
|
||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
|
||||
return
|
||||
}
|
||||
req = ctx.Task.Meta.Req
|
||||
}
|
||||
},
|
||||
)
|
||||
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
|
||||
gopeed := &Instance{
|
||||
Events: make(InstanceEvents),
|
||||
@@ -314,7 +340,9 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
|
||||
if req.Labels == nil {
|
||||
req.Labels = make(map[string]string)
|
||||
}
|
||||
engine := engine.NewEngine()
|
||||
engine := engine.NewEngine(&engine.Config{
|
||||
ProxyURL: d.cfg.ProxyUrl(),
|
||||
})
|
||||
defer engine.Close()
|
||||
err = engine.Runtime.Set("gopeed", gopeed)
|
||||
if err != nil {
|
||||
@@ -332,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)
|
||||
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 {
|
||||
@@ -566,10 +601,10 @@ func (h InstanceEvents) OnStart(fn goja.Callable) {
|
||||
h.register(EventOnStart, fn)
|
||||
}
|
||||
|
||||
//func (h InstanceEvents) OnError(fn goja.Callable) {
|
||||
// h.register(HookEventOnError, fn)
|
||||
//}
|
||||
//
|
||||
func (h InstanceEvents) OnError(fn goja.Callable) {
|
||||
h.register(EventOnError, fn)
|
||||
}
|
||||
|
||||
//func (h InstanceEvents) OnDone(fn goja.Callable) {
|
||||
// h.register(HookEventOnDone, fn)
|
||||
//}
|
||||
@@ -598,28 +633,28 @@ type InstanceLogger struct {
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func (l *InstanceLogger) Debug(msg ...any) {
|
||||
func (l *InstanceLogger) Debug(msg ...goja.Value) {
|
||||
if l.devMode {
|
||||
l.logger.Debug().Msg(l.append(msg...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *InstanceLogger) Info(msg ...any) {
|
||||
func (l *InstanceLogger) Info(msg ...goja.Value) {
|
||||
l.logger.Info().Msg(l.append(msg...))
|
||||
}
|
||||
|
||||
func (l *InstanceLogger) Warn(msg ...any) {
|
||||
func (l *InstanceLogger) Warn(msg ...goja.Value) {
|
||||
l.logger.Warn().Msg(l.append(msg...))
|
||||
}
|
||||
|
||||
func (l *InstanceLogger) Error(msg ...any) {
|
||||
func (l *InstanceLogger) Error(msg ...goja.Value) {
|
||||
l.logger.Error().Msg(l.append(msg...))
|
||||
}
|
||||
|
||||
func (l *InstanceLogger) append(msg ...any) string {
|
||||
func (l *InstanceLogger) append(msg ...goja.Value) string {
|
||||
strMsg := make([]string, len(msg))
|
||||
for i, m := range msg {
|
||||
strMsg[i] = fmt.Sprint(m)
|
||||
strMsg[i] = m.String()
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " "))
|
||||
}
|
||||
@@ -638,7 +673,33 @@ type OnResolveContext 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 {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
||||
"github.com/dop251/goja"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -164,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) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
|
||||
@@ -191,10 +193,10 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
|
||||
}
|
||||
task := downloader.GetTask(id)
|
||||
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" {
|
||||
panic("extension on start modify label error")
|
||||
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -206,13 +208,54 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
|
||||
|
||||
// label match
|
||||
downloadAndCheck(&base.Request{
|
||||
URL: "https://xxx.com",
|
||||
URL: "https://test.com",
|
||||
Labels: map[string]string{
|
||||
"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) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil {
|
||||
@@ -243,6 +286,26 @@ func TestDownloader_Extension_Errors(t *testing.T) {
|
||||
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) {
|
||||
@@ -335,31 +398,15 @@ func TestDownloader_DeleteExtension(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_Extension_OnResolve(t *testing.T) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := downloader.DeleteExtension(installedExt.Identity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
extensions := downloader.GetExtensions()
|
||||
if len(extensions) != 0 {
|
||||
t.Fatal("extension delete fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_Extension_Logger(t *testing.T) {
|
||||
logger := logger.NewLogger(false, "")
|
||||
il := newInstanceLogger(&Extension{
|
||||
Name: "test",
|
||||
}, logger)
|
||||
il.Debug("msg1", "msg2")
|
||||
il.Info("msg1", "msg2")
|
||||
il.Warn("msg1", "msg2")
|
||||
il.Error("msg1", "msg2")
|
||||
il.Debug(goja.NaN(), goja.Undefined())
|
||||
il.Info(goja.NaN(), goja.Undefined())
|
||||
il.Warn(goja.NaN(), goja.Undefined())
|
||||
il.Error(goja.NaN(), goja.Undefined())
|
||||
}
|
||||
|
||||
func setupDownloader(fn func(downloader *Downloader)) {
|
||||
|
||||
+38
-4
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -21,12 +23,15 @@ type Task struct {
|
||||
ID string `json:"id"`
|
||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||
Status base.Status `json:"status"`
|
||||
Protocol string `json:"protocol"`
|
||||
Progress *Progress `json:"progress"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
fetcherBuilder fetcher.FetcherBuilder
|
||||
fetcher fetcher.Fetcher
|
||||
timer *util.Timer
|
||||
statusLock *sync.Mutex
|
||||
lock *sync.Mutex
|
||||
speedArr []int64
|
||||
}
|
||||
@@ -40,9 +45,15 @@ func NewTask() *Task {
|
||||
ID: id,
|
||||
Status: base.DownloadStatusReady,
|
||||
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 {
|
||||
return &Task{
|
||||
ID: t.ID,
|
||||
@@ -50,6 +61,7 @@ func (t *Task) clone() *Task {
|
||||
Status: t.Status,
|
||||
Progress: t.Progress,
|
||||
CreatedAt: t.CreatedAt,
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,15 +113,37 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
|
||||
type DownloaderStoreConfig struct {
|
||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||
|
||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||
Extra map[string]any `json:"extra"` // Extra is the extra config
|
||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||
Extra map[string]any `json:"extra"`
|
||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.MaxRunning == 0 {
|
||||
cfg.MaxRunning = 5
|
||||
}
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (cfg *DownloaderStoreConfig) ProxyUrl() *url.URL {
|
||||
if cfg.Proxy == nil {
|
||||
return nil
|
||||
}
|
||||
if cfg.Proxy.Enable == false || cfg.Proxy.Scheme == "" || cfg.Proxy.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return util.BuildProxyUrl(cfg.Proxy.Scheme, cfg.Proxy.Host, cfg.Proxy.Usr, cfg.Proxy.Pwd)
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
Scheme string `json:"scheme"`
|
||||
Host string `json:"host"`
|
||||
Usr string `json:"usr"`
|
||||
Pwd string `json:"pwd"`
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
const aaa = {};
|
||||
// access undefined property
|
||||
gopeed.logger.info(aaa.bbb.ccc);
|
||||
|
||||
ctx.res = {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
throw new MessageError("test");
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "message-error",
|
||||
"title": "gopeed extension message error test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://github.com/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
gopeed.events.onError(async function (ctx) {
|
||||
gopeed.logger.info("url", ctx.task.meta.req.url);
|
||||
gopeed.logger.info("error", ctx.error);
|
||||
ctx.task.meta.req.url = "https://github.com";
|
||||
ctx.task.pause();
|
||||
ctx.task.continue();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "on-error",
|
||||
"title": "gopeed extension on error event test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onError",
|
||||
"match": {
|
||||
"labels": [
|
||||
"test"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
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.labels['modified'] = 'true';
|
||||
});
|
||||
|
||||
@@ -3,3 +3,12 @@ package bt
|
||||
type ReqExtra struct {
|
||||
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,4 +8,12 @@ type ReqExtra struct {
|
||||
|
||||
type OptsExtra struct {
|
||||
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
@@ -104,6 +104,17 @@ func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
vars := mux.Vars(r)
|
||||
taskId := vars["id"]
|
||||
@@ -122,20 +133,8 @@ func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||
func GetTasks(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
status := r.Form["status"]
|
||||
tasks := Downloader.GetTasks()
|
||||
if len(status) == 0 {
|
||||
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))
|
||||
tasks := Downloader.GetTasksByStatues(convertStatues(status))
|
||||
WriteJson(w, model.NewOkResult(tasks))
|
||||
}
|
||||
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -282,6 +281,29 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(msg))
|
||||
|
||||
+59
-14
@@ -3,11 +3,13 @@ package rest
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"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/continue").HandlerFunc(ContinueAllTask)
|
||||
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").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.MethodPut).Path("/api/v1/config").HandlerFunc(PutConfig)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/extensions").HandlerFunc(InstallExtension)
|
||||
@@ -105,9 +109,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension)
|
||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
||||
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
||||
if startCfg.WebEnable {
|
||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
|
||||
}
|
||||
|
||||
@@ -130,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(
|
||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||
@@ -138,30 +157,56 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
return srv, listener, nil
|
||||
}
|
||||
|
||||
func resolvePath(urlPath string, prefix string) (identity string, path string, err error) {
|
||||
// remove prefix
|
||||
clearPath := strings.TrimPrefix(urlPath, prefix)
|
||||
// match extension identity, eg: /fs/extensions/identity/xxx
|
||||
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
|
||||
if !reg.MatchString(clearPath) {
|
||||
err = os.ErrNotExist
|
||||
return
|
||||
}
|
||||
matched := reg.FindStringSubmatch(clearPath)
|
||||
if len(matched) != 3 {
|
||||
err = os.ErrNotExist
|
||||
return
|
||||
}
|
||||
return matched[1], matched[2], nil
|
||||
}
|
||||
|
||||
// handle task file resource
|
||||
type taskFileSystem struct {
|
||||
}
|
||||
|
||||
func (e *taskFileSystem) Open(name string) (http.File, error) {
|
||||
// get extension identity
|
||||
identity, path, err := resolvePath(name, "/fs/tasks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task := Downloader.GetTask(identity)
|
||||
if task == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return os.Open(filepath.Join(task.Meta.RootDirPath(), path))
|
||||
}
|
||||
|
||||
// handle extension file resource
|
||||
type extensionFileSystem struct {
|
||||
}
|
||||
|
||||
func (e *extensionFileSystem) Open(name string) (http.File, error) {
|
||||
// remove prefix
|
||||
path := strings.TrimPrefix(name, "/fs/extensions")
|
||||
// match extension identity, eg: /fs/extensions/identity/xxx
|
||||
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
|
||||
if !reg.MatchString(path) {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
matched := reg.FindStringSubmatch(path)
|
||||
if len(matched) != 3 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
// get extension identity
|
||||
identity := matched[1]
|
||||
identity, path, err := resolvePath(name, "/fs/extensions")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
extension, err := Downloader.GetExtension(identity)
|
||||
if err != nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
extensionPath := Downloader.ExtensionPath(extension)
|
||||
return os.Open(filepath.Join(extensionPath, matched[2]))
|
||||
return os.Open(filepath.Join(extensionPath, path))
|
||||
}
|
||||
|
||||
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
||||
|
||||
+57
-2
@@ -27,7 +27,7 @@ var (
|
||||
Extra: map[string]any{
|
||||
"method": "",
|
||||
"header": map[string]string{
|
||||
"User-Agent": "gopeed",
|
||||
"Usr-Agent": "gopeed",
|
||||
},
|
||||
"body": "",
|
||||
},
|
||||
@@ -66,7 +66,7 @@ var (
|
||||
func TestResolve(t *testing.T) {
|
||||
doTest(func() {
|
||||
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))
|
||||
}
|
||||
})
|
||||
@@ -204,12 +204,14 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
|
||||
|
||||
// continue all
|
||||
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)
|
||||
if len(tasks) != cfg.MaxRunning {
|
||||
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
|
||||
}
|
||||
// pause all
|
||||
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)
|
||||
if 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) {
|
||||
doTest(func() {
|
||||
var wg sync.WaitGroup
|
||||
@@ -491,6 +545,7 @@ func doTest(handler func()) {
|
||||
cfg.Init()
|
||||
cfg.Storage = storage
|
||||
cfg.StorageDir = ".test_storage"
|
||||
cfg.WebEnable = true
|
||||
fileListener := doStart(cfg)
|
||||
defer func() {
|
||||
if err := fileListener.Close(); err != nil {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
"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 {
|
||||
if size == 0 {
|
||||
|
||||
@@ -183,3 +183,14 @@ func IsExistsFile(path string) bool {
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package util
|
||||
|
||||
var invalidPathChars = []string{`/`, `:`}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package util
|
||||
|
||||
var invalidPathChars = []string{`\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`}
|
||||
@@ -2,6 +2,7 @@ package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@@ -37,3 +38,16 @@ func ParseDataUri(uri string) (string, []byte) {
|
||||
}
|
||||
return mime, data
|
||||
}
|
||||
|
||||
// BuildProxyUrl builds a proxy url with given host, username and password.
|
||||
func BuildProxyUrl(scheme, host, usr, pwd string) *url.URL {
|
||||
var user *url.Userinfo
|
||||
if usr != "" && pwd != "" {
|
||||
user = url.UserPassword(usr, pwd)
|
||||
}
|
||||
return &url.URL{
|
||||
Scheme: scheme,
|
||||
User: user,
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
@@ -63,4 +63,8 @@ windows/flutter/generated_plugin_registrant.cc
|
||||
windows/flutter/generated_plugin_registrant.h
|
||||
windows/flutter/generated_plugins.cmake
|
||||
|
||||
/extensions
|
||||
/extensions
|
||||
|
||||
# Hive database files
|
||||
database.hive
|
||||
database.lock
|
||||
@@ -71,6 +71,13 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Add android:stopWithTask option only when necessary. -->
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024px" height="1024px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g><path style="opacity:0.998" fill="#5dc76c" d="M 951.5,763.5 C 940.739,755.276 929.405,747.776 917.5,741C 908.477,736.319 899.143,732.485 889.5,729.5C 885.844,625.779 840.844,545.613 754.5,489C 674.802,441.453 590.135,430.12 500.5,455C 419.755,480.605 359.588,530.771 320,605.5C 278.585,565.816 230.752,555.649 176.5,575C 131.948,594.935 106.448,629.102 100,677.5C 99.3333,685.833 99.3333,694.167 100,702.5C 102.081,714.824 105.081,726.824 109,738.5C 95.4882,745.492 82.9882,753.826 71.5,763.5C 37.7121,705.639 16.5454,643.306 8,576.5C -9.41698,416.759 35.9164,278.093 144,160.5C 244.661,58.2827 367.161,5.94939 511.5,3.5C 678.975,7.48829 814.142,74.8216 917,205.5C 1011.33,336.426 1040,480.759 1003,638.5C 991.564,682.671 974.397,724.337 951.5,763.5 Z"/></g>
|
||||
<g><path style="opacity:0.999" fill="#fefefe" d="M 951.5,763.5 C 949.28,768.272 946.78,772.938 944,777.5C 938.305,786.913 932.138,795.913 925.5,804.5C 915.954,796.888 905.288,791.055 893.5,787C 878.282,782.958 863.115,778.791 848,774.5C 854.013,716.891 841.346,663.891 810,615.5C 768.341,553.228 711.174,512.395 638.5,493C 560.311,479.566 490.311,497.566 428.5,547C 411.963,560.536 397.13,575.703 384,592.5C 366.745,616.923 348.911,640.923 330.5,664.5C 310.135,651.302 290.469,637.135 271.5,622C 238.448,602.307 206.782,604.307 176.5,628C 159.375,642.752 148.875,661.252 145,683.5C 143.968,692.716 144.968,701.716 148,710.5C 154.334,725.339 160.167,740.339 165.5,755.5C 138.118,764.688 114.451,779.688 94.5,800.5C 85.653,788.968 77.9863,776.635 71.5,763.5C 82.9882,753.826 95.4882,745.492 109,738.5C 105.081,726.824 102.081,714.824 100,702.5C 99.3333,694.167 99.3333,685.833 100,677.5C 106.448,629.102 131.948,594.935 176.5,575C 230.752,555.649 278.585,565.816 320,605.5C 359.588,530.771 419.755,480.605 500.5,455C 590.135,430.12 674.802,441.453 754.5,489C 840.844,545.613 885.844,625.779 889.5,729.5C 899.143,732.485 908.477,736.319 917.5,741C 929.405,747.776 940.739,755.276 951.5,763.5 Z"/></g>
|
||||
<g><path style="opacity:0.998" fill="#5dc76c" d="M 925.5,804.5 C 850.791,908.608 750.791,975.775 625.5,1006C 609.308,1009.7 592.975,1012.53 576.5,1014.5C 615.708,969.938 655.042,925.438 694.5,881C 664.168,880.5 633.835,880.333 603.5,880.5C 603.5,811.167 603.5,741.833 603.5,672.5C 542.167,672.5 480.833,672.5 419.5,672.5C 419.5,741.5 419.5,810.5 419.5,879.5C 388.493,879.167 357.493,879.5 326.5,880.5C 326.624,881.107 326.957,881.44 327.5,881.5C 366.805,925.808 406.138,970.141 445.5,1014.5C 318.63,996.317 212.796,939.651 128,844.5C 115.85,830.525 104.683,815.859 94.5,800.5C 114.451,779.688 138.118,764.688 165.5,755.5C 160.167,740.339 154.334,725.339 148,710.5C 144.968,701.716 143.968,692.716 145,683.5C 148.875,661.252 159.375,642.752 176.5,628C 206.782,604.307 238.448,602.307 271.5,622C 290.469,637.135 310.135,651.302 330.5,664.5C 348.911,640.923 366.745,616.923 384,592.5C 397.13,575.703 411.963,560.536 428.5,547C 490.311,497.566 560.311,479.566 638.5,493C 711.174,512.395 768.341,553.228 810,615.5C 841.346,663.891 854.013,716.891 848,774.5C 863.115,778.791 878.282,782.958 893.5,787C 905.288,791.055 915.954,796.888 925.5,804.5 Z"/></g>
|
||||
<g><path style="opacity:0.999" fill="#fefefe" d="M 576.5,1014.5 C 532.836,1019.82 489.169,1019.82 445.5,1014.5C 406.138,970.141 366.805,925.808 327.5,881.5C 358.167,880.833 388.833,880.167 419.5,879.5C 419.5,810.5 419.5,741.5 419.5,672.5C 480.833,672.5 542.167,672.5 603.5,672.5C 603.5,741.833 603.5,811.167 603.5,880.5C 633.835,880.333 664.168,880.5 694.5,881C 655.042,925.438 615.708,969.938 576.5,1014.5 Z"/></g>
|
||||
<g><path style="opacity:1" fill="#ade2b5" d="M 419.5,879.5 C 388.833,880.167 358.167,880.833 327.5,881.5C 326.957,881.44 326.624,881.107 326.5,880.5C 357.493,879.5 388.493,879.167 419.5,879.5 Z"/></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"label": "English",
|
||||
"error": "Error",
|
||||
"tip": "Tip",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"selectAll": "Select All",
|
||||
"task": "Tasks",
|
||||
"downloading": "downloading",
|
||||
"downloaded": "downloaded",
|
||||
"setting": "Settings",
|
||||
"donate": "Donate",
|
||||
"exit": "Exit",
|
||||
"create": "Create Task",
|
||||
"directDownload": "Direct Download",
|
||||
"advancedOptions": "Advanced Options",
|
||||
"downloadLink": "Download Link",
|
||||
"downloadLinkValid": "Please enter the download link",
|
||||
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
|
||||
"downloadLinkHitDesktop": ", or drag the torrent file here directly",
|
||||
"download": "Download",
|
||||
"noFileSelected": "Please select at least one file to continue.",
|
||||
"noStoragePermission": "Storage permission required",
|
||||
"selectFile": "Select File",
|
||||
"rename": "Rename",
|
||||
"basic": "Basic",
|
||||
"advanced": "Advanced",
|
||||
"general": "General",
|
||||
"downloadDir": "Download Directory",
|
||||
"downloadDirValid": "Please select the download directory",
|
||||
"connections": "Connections",
|
||||
"maxRunning": "Max Running Tasks",
|
||||
"items": "@count items",
|
||||
"subscribeTracker": "Subscribe Tracker",
|
||||
"subscribeFail": "Subscribe failed, please check network or try again later",
|
||||
"update": "Update",
|
||||
"updateDaily": "Update daily",
|
||||
"lastUpdate": "Last update: @time",
|
||||
"addTracker": "Add Tracker",
|
||||
"addTrackerHit": "Please enter the tracker server url, one per line",
|
||||
"ui": "UI",
|
||||
"theme": "Theme",
|
||||
"themeSystem": "System",
|
||||
"themeLight": "Light",
|
||||
"themeDark": "Dark",
|
||||
"locale": "Language",
|
||||
"about": "About",
|
||||
"homepage": "Homepage",
|
||||
"version": "Version",
|
||||
"protocol": "Protocol",
|
||||
"port": "Port",
|
||||
"apiToken": "API Token",
|
||||
"notSet": "NS",
|
||||
"set": "SET",
|
||||
"effectAfterRestart": "Effect after restart",
|
||||
"startAll": "Start All",
|
||||
"pauseAll": "Pause All",
|
||||
"deleteTask": "Delete Task",
|
||||
"deleteTaskTip": "Keep downloaded files",
|
||||
"delete": "Delete",
|
||||
"newVersionTitle": "Discover new version @version",
|
||||
"newVersionUpdate": "Update Now",
|
||||
"newVersionLater": "Later",
|
||||
"extensions": "Extensions",
|
||||
"extensionInstallUrl": "Install URL",
|
||||
"extensionInstallSuccess": "Installed successfully",
|
||||
"extensionUpdateSuccess": "Updated successfully",
|
||||
"extensionDelete": "Delete Extension",
|
||||
"extensionAlreadyLatest": "It's already the latest version",
|
||||
"extensionFind": "Find Extensions",
|
||||
"extensionDevelop": "Develop Extensions",
|
||||
"history": "History",
|
||||
"clearHistory": "Clear History",
|
||||
"noHistoryFound": "No History Found",
|
||||
"serviceTitle": "Download Service",
|
||||
"serviceText": "Running"
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"label": "فارسی",
|
||||
"error": "اررور",
|
||||
"tip": "Tip",
|
||||
"confirm": "تایید",
|
||||
"cancel": "انصراف",
|
||||
"selectAll": "انتخاب همه",
|
||||
"task": "کار",
|
||||
"setting": "تنظیمات",
|
||||
"donate": "کمک مالی",
|
||||
"exit": "خروج",
|
||||
"create": "ایجاد کار",
|
||||
"downloading": "دانلود کردن",
|
||||
"downloaded": "دانلود شدن",
|
||||
"downloadLink": "لینک دانلود",
|
||||
"downloadLinkValid": "لطفا لینک دانلود را وارد کنید",
|
||||
"download": "دانلود",
|
||||
"noFileSelected": "لطفا حداقل یک فایل را جهت دانلود انتخاب کنید.",
|
||||
"noStoragePermission": "مجوز ذخیره سازی مورد نیاز است",
|
||||
"selectFile": "انتخاب فایل",
|
||||
"basic": "پایه",
|
||||
"advanced": "پیشرفته",
|
||||
"general": "عمومی",
|
||||
"downloadDir": "دایرکتوری دانلود",
|
||||
"downloadDirValid": "لطفا دایرکتوری دانلود را انتخاب کنید",
|
||||
"connections": "اتصالات",
|
||||
"items": "@count آیتم ها",
|
||||
"subscribeTracker": "Subscribe Tracker",
|
||||
"subscribeFail": "Subscribe failed, please check network or try again later",
|
||||
"update": "بروزرسانی",
|
||||
"updateDaily": "بروزرسانی روزانه",
|
||||
"lastUpdate": "Last update: @time",
|
||||
"addTracker": "Add Tracker",
|
||||
"addTrackerHit": "Please enter the tracker server url, one per line",
|
||||
"ui": "رابط کاربری",
|
||||
"theme": "پوسته",
|
||||
"themeSystem": "سیستم",
|
||||
"themeLight": "روشن",
|
||||
"themeDark": "تاریک",
|
||||
"locale": "Language",
|
||||
"about": "درباره ی",
|
||||
"homepage": "صفحه اصلی",
|
||||
"version": "نسخه",
|
||||
"protocol": "پروتکل",
|
||||
"port": "پورت",
|
||||
"apiToken": "API توکن",
|
||||
"notSet": "NS",
|
||||
"set": "SET",
|
||||
"effectAfterRestart": "Effect after restart",
|
||||
"startAll": "شروع همه",
|
||||
"pauseAll": "توقف همه",
|
||||
"deleteTask": "پاک کردن کار",
|
||||
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
|
||||
"delete": "پاک کردن",
|
||||
"newVersionTitle": "@version عنوان: کشف نسخه جدید",
|
||||
"newVersionUpdate": "بروزرسانی",
|
||||
"newVersionLater": "بعداً"
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"label": "日本語",
|
||||
"error": "エラー",
|
||||
"tip": "ヒント",
|
||||
"confirm": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"selectAll": "すべてを選択",
|
||||
"task": "タスク",
|
||||
"downloading": "ダウンロード中",
|
||||
"downloaded": "ダウンロード済",
|
||||
"setting": "セッティング",
|
||||
"donate": "寄付する",
|
||||
"exit": "終了",
|
||||
"create": "タスクを作成",
|
||||
"downloadLink": "ダウンロードリンク",
|
||||
"downloadLinkValid": "ダウンロードリンクを入力してください",
|
||||
"downloadLinkHit": "ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append",
|
||||
"downloadLinkHitDesktop": "または、torrent ファイルを直接ここにドラッグします",
|
||||
"download": "ダウンロード",
|
||||
"noFileSelected": "続行するには少なくとも 1 つのファイルを選択してください。",
|
||||
"noStoragePermission": "ストレージのパーミッションが必要",
|
||||
"selectFile": "ファイルを選択",
|
||||
"basic": "ベーシック",
|
||||
"advanced": "アドバンスド",
|
||||
"general": "ジェネラル",
|
||||
"downloadDir": "ディレクトリのダウンロード",
|
||||
"downloadDirValid": "ダウンロードディレクトリを選択してください",
|
||||
"connections": "接続",
|
||||
"maxRunning": "最大実行タスク",
|
||||
"items": "@count アイテム",
|
||||
"subscribeTracker": "トラッカーを購読",
|
||||
"subscribeFail": "登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください",
|
||||
"update": "アップデート",
|
||||
"updateDaily": "毎日アップデート",
|
||||
"lastUpdate": "最終アップデート: @time",
|
||||
"addTracker": "トラッカーを追加",
|
||||
"addTrackerHit": "トラッカーサーバーの URL を 1 行に 1 つずつ入力してください",
|
||||
"ui": "UI",
|
||||
"theme": "テーマ",
|
||||
"themeSystem": "システム",
|
||||
"themeLight": "ライト",
|
||||
"themeDark": "ダーク",
|
||||
"locale": "言語",
|
||||
"about": "概要",
|
||||
"homepage": "ホームページ",
|
||||
"version": "バージョン",
|
||||
"protocol": "プロトコル",
|
||||
"port": "ポート",
|
||||
"apiToken": "API トークン",
|
||||
"notSet": "未設置",
|
||||
"set": "設置",
|
||||
"effectAfterRestart": "再起動後の効果",
|
||||
"startAll": "すべてを開始",
|
||||
"pauseAll": "すべてを一時停止",
|
||||
"deleteTask": "タスクを削除",
|
||||
"deleteTaskTip": "ダウンロードしたファイルを保持",
|
||||
"delete": "削除",
|
||||
"newVersionTitle": "新しいバージョン @version を発見する",
|
||||
"newVersionUpdate": "アップデート",
|
||||
"newVersionLater": "後で"
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"label": "Русский(Россия)",
|
||||
"error": "Ошибка",
|
||||
"tip": "Подсказка",
|
||||
"confirm": "Подтвердить",
|
||||
"cancel": "Отмена",
|
||||
"selectAll": "Выбрать все",
|
||||
"task": "Задачи",
|
||||
"downloading": "Загрузка",
|
||||
"downloaded": "Загружено",
|
||||
"setting": "Настройки",
|
||||
"donate": "Пожертвовать",
|
||||
"exit": "Выход",
|
||||
"create": "Создать задачу",
|
||||
"advancedOptions": "Расширенные опции",
|
||||
"downloadLink": "Ссылка на скачивание",
|
||||
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
|
||||
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append",
|
||||
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
|
||||
"download": "Скачать",
|
||||
"noFileSelected": "Файл не выбран",
|
||||
"noStoragePermission": "Требуется доступ к хранилищу",
|
||||
"selectFile": "Выбрать файл",
|
||||
"rename": "Переименовать",
|
||||
"basic": "Основные параметры",
|
||||
"advanced": "Расширенные",
|
||||
"general": "Общие",
|
||||
"downloadDir": "Папка загрузки",
|
||||
"downloadDirValid": "Пожалуйста, выберите папку для загрузки",
|
||||
"connections": "Соединений",
|
||||
"maxRunning": "Максимальное количество активных задач",
|
||||
"items": "@подсчет элементов",
|
||||
"subscribeTracker": "Подпись трекера",
|
||||
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
|
||||
"update": "Обновить",
|
||||
"updateDaily": "Обновлять ежедневно",
|
||||
"lastUpdate": "Последнее обновление: @время",
|
||||
"addTracker": "Добавить трекер",
|
||||
"addTrackerHit": "Пожалуйста, введите url трекера, по одному в строке",
|
||||
"ui": "UI",
|
||||
"theme": "Тема",
|
||||
"themeSystem": "Системная",
|
||||
"themeLight": "Светлая",
|
||||
"themeDark": "Темная",
|
||||
"locale": "Язык",
|
||||
"about": "О приложении",
|
||||
"homepage": "Домашняя страница",
|
||||
"version": "Версия",
|
||||
"protocol": "Протокол",
|
||||
"port": "Порт",
|
||||
"apiToken": "API Токен",
|
||||
"notSet": "Не установлено",
|
||||
"set": "Установлено",
|
||||
"effectAfterRestart": "Эффект после перезагрузки",
|
||||
"startAll": "Запустить все",
|
||||
"pauseAll": "Приостановить все",
|
||||
"deleteTask": "Удалить задачу",
|
||||
"deleteTaskTip": "Сохранить загруженные файлы",
|
||||
"delete": "Удалить",
|
||||
"newVersionTitle": "Обнаружена новая версия @version",
|
||||
"newVersionUpdate": "Обновить",
|
||||
"newVersionLater": "позже",
|
||||
"extensions": "Расширения",
|
||||
"extensionInstallSuccess": "Установка завершена",
|
||||
"extensionUpdateSuccess": "Обновление завершено",
|
||||
"extensionDelete": "Удалить расширение",
|
||||
"extensionAlreadyLatest": "Это последняя версия",
|
||||
"extensionFind": "Найти расширения",
|
||||
"extensionDevelop": "Разработка расширений",
|
||||
"history": "История",
|
||||
"clearHistory": "Очистить историю",
|
||||
"noHistoryFound": "История не найдена"
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"label": "中文(简体)",
|
||||
"error": "错误",
|
||||
"tip": "提示",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"selectAll": "全选",
|
||||
"task": "任务",
|
||||
"downloading": "下载中",
|
||||
"downloaded": "已完成",
|
||||
"setting": "设置",
|
||||
"donate": "打赏",
|
||||
"exit": "退出",
|
||||
"create": "创建任务",
|
||||
"directDownload": "直接下载",
|
||||
"advancedOptions": "高级选项",
|
||||
"downloadLink": "下载链接",
|
||||
"downloadLinkValid": "请输入下载链接",
|
||||
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
|
||||
"downloadLinkHitDesktop": ",也可以直接拖拽种子文件到此处",
|
||||
"download": "下载",
|
||||
"noFileSelected": "请至少选择一个文件下载",
|
||||
"noStoragePermission": "需要开启存储权限",
|
||||
"selectFile": "选择文件",
|
||||
"rename": "重命名",
|
||||
"basic": "基础",
|
||||
"advanced": "高级",
|
||||
"general": "通用",
|
||||
"downloadDir": "下载目录",
|
||||
"downloadDirValid": "请选择下载目录",
|
||||
"connections": "连接数",
|
||||
"maxRunning": "最大下载数",
|
||||
"items": "@count 项",
|
||||
"subscribeTracker": "订阅 Tracker",
|
||||
"subscribeFail": "订阅失败,请检查网络或稍后重试",
|
||||
"update": "更新",
|
||||
"updateDaily": "每天自动更新",
|
||||
"lastUpdate": "上次更新:@time",
|
||||
"addTracker": "添加 Tracker",
|
||||
"addTrackerHit": "请输入 tracker 服务器地址,每行一条",
|
||||
"ui": "界面",
|
||||
"theme": "主题",
|
||||
"themeSystem": "跟随系统",
|
||||
"themeLight": "明亮主题",
|
||||
"themeDark": "暗黑主题",
|
||||
"locale": "语言",
|
||||
"about": "关于",
|
||||
"homepage": "主页",
|
||||
"version": "版本",
|
||||
"protocol": "通讯协议",
|
||||
"port": "端口",
|
||||
"apiToken": "接口令牌",
|
||||
"notSet": "未设置",
|
||||
"set": "已设置",
|
||||
"effectAfterRestart": "此配置项将在重启应用后生效",
|
||||
"startAll": "全部开始",
|
||||
"pauseAll": "全部暂停",
|
||||
"deleteTask": "删除任务",
|
||||
"deleteTaskTip": "保留已下载的文件",
|
||||
"delete": "删除",
|
||||
"newVersionTitle": "发现新版本 @version",
|
||||
"newVersionUpdate": "立即更新",
|
||||
"newVersionLater": "稍后再说",
|
||||
"extensions": "扩展",
|
||||
"extensionInstallUrl": "安装链接",
|
||||
"extensionInstallSuccess": "安装成功",
|
||||
"extensionUpdateSuccess": "更新成功",
|
||||
"extensionDelete": "删除扩展",
|
||||
"extensionAlreadyLatest": "已经是最新版本",
|
||||
"extensionFind": "获取扩展",
|
||||
"extensionDevelop": "开发扩展",
|
||||
"history": "历史记录",
|
||||
"clearHistory": "清空历史记录",
|
||||
"noHistoryFound": "暂无历史记录",
|
||||
"serviceTitle": "下载服务",
|
||||
"serviceText": "运行中"
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
{
|
||||
"label": "中文 (正體) ",
|
||||
"error": "錯誤",
|
||||
"tip": "提示",
|
||||
"confirm": "確定",
|
||||
"cancel": "取消",
|
||||
"selectAll": "全選",
|
||||
"task": "任務",
|
||||
"downloading": "下載中",
|
||||
"downloaded": "已完成",
|
||||
"setting": "設定",
|
||||
"donate": "斗內",
|
||||
"exit": "離開",
|
||||
"create": "新增任務",
|
||||
"advancedOptions": "進階選項",
|
||||
"downloadLink": "下載鏈接",
|
||||
"downloadLinkValid": "請輸入下載鏈接",
|
||||
"downloadLinkHit": "請輸入下載鏈接,支援 HTTP/HTTPS/MAGNET@append",
|
||||
"downloadLinkHitDesktop": ",也可以通過拖拽Torrent文件至此以新增任務",
|
||||
"download": "下載",
|
||||
"noFileSelected": "沒有選取的文件",
|
||||
"noStoragePermission": "沒有存儲權限",
|
||||
"selectFile": "選擇文件",
|
||||
"rename": "重命名",
|
||||
"basic": "基礎",
|
||||
"advanced": "進階",
|
||||
"general": "通用設定",
|
||||
"downloadDir": "下載目録",
|
||||
"downloadDirValid": "請選擇下載目録",
|
||||
"connections": "同時連接數量",
|
||||
"maxRunning": "最大同時下載數",
|
||||
"items": "@count 項",
|
||||
"subscribeTracker": "已訂閲的 Tracker",
|
||||
"subscribeFail": "訂閲失敗",
|
||||
"update": "更新",
|
||||
"updateDaily": "每日自動更新",
|
||||
"lastUpdate": "上次更新時間: @time",
|
||||
"addTracker": "增加新的 Tracker",
|
||||
"addTrackerHit": "請輸入 Tracker 服務器地址,每行一條",
|
||||
"ui": "介面",
|
||||
"theme": "佈景主題",
|
||||
"themeSystem": "跟隨系統",
|
||||
"themeLight": "亮色主題",
|
||||
"themeDark": "深色主題",
|
||||
"locale": "語言",
|
||||
"about": "關於",
|
||||
"homepage": "訪問主頁",
|
||||
"version": "版本資訊",
|
||||
"protocol": "通訊協定",
|
||||
"port": "端口",
|
||||
"apiToken": "接口 Token",
|
||||
"notSet": "尚未設定",
|
||||
"set": "已設定",
|
||||
"effectAfterRestart": "該選項將在重新啓動軟體後生效",
|
||||
"startAll": "全部開始",
|
||||
"pauseAll": "全部暫停",
|
||||
"deleteTask": "刪除任務",
|
||||
"deleteTaskTip": "保留已經下載的文件",
|
||||
"delete": "刪除",
|
||||
"newVersionTitle": "發現新版本 @version",
|
||||
"newVersionUpdate": "立刻更新",
|
||||
"newVersionLater": "稍後更新",
|
||||
"extensions": "外掛程序",
|
||||
"extensionInstallUrl": "安裝鏈接",
|
||||
"extensionInstallSuccess": "安裝完成",
|
||||
"extensionUpdateSuccess": "更新完成",
|
||||
"extensionDelete": "解除安裝",
|
||||
"extensionAlreadyLatest": "已是最新版本"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
output: dist/
|
||||
@@ -4,12 +4,16 @@ part 'downloader_config.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class DownloaderConfig {
|
||||
String downloadDir = '';
|
||||
int maxRunning = 0;
|
||||
String downloadDir;
|
||||
int maxRunning;
|
||||
ProtocolConfig protocolConfig = ProtocolConfig();
|
||||
ExtraConfig extra = ExtraConfig();
|
||||
ProxyConfig proxy = ProxyConfig();
|
||||
|
||||
DownloaderConfig();
|
||||
DownloaderConfig({
|
||||
this.downloadDir = '',
|
||||
this.maxRunning = 0,
|
||||
});
|
||||
|
||||
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$DownloaderConfigFromJson(json);
|
||||
@@ -30,9 +34,16 @@ class ProtocolConfig {
|
||||
|
||||
@JsonSerializable()
|
||||
class HttpConfig {
|
||||
int connections = 0;
|
||||
String userAgent;
|
||||
int connections;
|
||||
bool useServerCtime;
|
||||
|
||||
HttpConfig();
|
||||
HttpConfig({
|
||||
this.userAgent =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
||||
this.connections = 16,
|
||||
this.useServerCtime = false,
|
||||
});
|
||||
|
||||
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$HttpConfigFromJson(json);
|
||||
@@ -41,6 +52,7 @@ class HttpConfig {
|
||||
|
||||
@JsonSerializable()
|
||||
class BtConfig {
|
||||
int listenPort = 0;
|
||||
List<String> trackers = [];
|
||||
|
||||
BtConfig();
|
||||
@@ -63,10 +75,33 @@ class ExtraConfig {
|
||||
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ProxyConfig {
|
||||
bool enable;
|
||||
String scheme;
|
||||
String host;
|
||||
String usr;
|
||||
String pwd;
|
||||
|
||||
ProxyConfig({
|
||||
this.enable = false,
|
||||
this.scheme = '',
|
||||
this.host = '',
|
||||
this.usr = '',
|
||||
this.pwd = '',
|
||||
});
|
||||
|
||||
factory ProxyConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProxyConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ProxyConfigToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ExtraConfigBt {
|
||||
List<String> trackerSubscribeUrls = [];
|
||||
List<String> subscribeTrackers = [];
|
||||
bool autoUpdateTrackers = true;
|
||||
DateTime? lastTrackerUpdateTime;
|
||||
|
||||
List<String> customTrackers = [];
|
||||
|
||||
@@ -7,12 +7,14 @@ part of 'downloader_config.dart';
|
||||
// **************************************************************************
|
||||
|
||||
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
|
||||
DownloaderConfig()
|
||||
..downloadDir = json['downloadDir'] as String
|
||||
..maxRunning = json['maxRunning'] as int
|
||||
DownloaderConfig(
|
||||
downloadDir: json['downloadDir'] as String? ?? '',
|
||||
maxRunning: json['maxRunning'] as int? ?? 0,
|
||||
)
|
||||
..protocolConfig = ProtocolConfig.fromJson(
|
||||
json['protocolConfig'] as Map<String, dynamic>?)
|
||||
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?);
|
||||
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?)
|
||||
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
|
||||
<String, dynamic>{
|
||||
@@ -20,6 +22,7 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
|
||||
'maxRunning': instance.maxRunning,
|
||||
'protocolConfig': instance.protocolConfig.toJson(),
|
||||
'extra': instance.extra.toJson(),
|
||||
'proxy': instance.proxy.toJson(),
|
||||
};
|
||||
|
||||
ProtocolConfig _$ProtocolConfigFromJson(Map<String, dynamic> json) =>
|
||||
@@ -33,19 +36,27 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
|
||||
'bt': instance.bt.toJson(),
|
||||
};
|
||||
|
||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) =>
|
||||
HttpConfig()..connections = json['connections'] as int;
|
||||
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig(
|
||||
userAgent: json['userAgent'] as String? ??
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
||||
connections: json['connections'] as int? ?? 16,
|
||||
useServerCtime: json['useServerCtime'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'userAgent': instance.userAgent,
|
||||
'connections': instance.connections,
|
||||
'useServerCtime': instance.useServerCtime,
|
||||
};
|
||||
|
||||
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
|
||||
..listenPort = json['listenPort'] as int
|
||||
..trackers =
|
||||
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
|
||||
|
||||
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
|
||||
'listenPort': instance.listenPort,
|
||||
'trackers': instance.trackers,
|
||||
};
|
||||
|
||||
@@ -61,6 +72,23 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
'bt': instance.bt.toJson(),
|
||||
};
|
||||
|
||||
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
|
||||
enable: json['enable'] as bool? ?? false,
|
||||
scheme: json['scheme'] as String? ?? '',
|
||||
host: json['host'] as String? ?? '',
|
||||
usr: json['usr'] as String? ?? '',
|
||||
pwd: json['pwd'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'enable': instance.enable,
|
||||
'scheme': instance.scheme,
|
||||
'host': instance.host,
|
||||
'usr': instance.usr,
|
||||
'pwd': instance.pwd,
|
||||
};
|
||||
|
||||
ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
|
||||
ExtraConfigBt()
|
||||
..trackerSubscribeUrls = (json['trackerSubscribeUrls'] as List<dynamic>)
|
||||
@@ -69,6 +97,7 @@ ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
|
||||
..subscribeTrackers = (json['subscribeTrackers'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList()
|
||||
..autoUpdateTrackers = json['autoUpdateTrackers'] as bool
|
||||
..lastTrackerUpdateTime = json['lastTrackerUpdateTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastTrackerUpdateTime'] as String)
|
||||
@@ -80,6 +109,7 @@ Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) {
|
||||
final val = <String, dynamic>{
|
||||
'trackerSubscribeUrls': instance.trackerSubscribeUrls,
|
||||
'subscribeTrackers': instance.subscribeTrackers,
|
||||
'autoUpdateTrackers': instance.autoUpdateTrackers,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
|
||||
@@ -12,7 +12,7 @@ class Options {
|
||||
Options({
|
||||
required this.name,
|
||||
required this.path,
|
||||
required this.selectFiles,
|
||||
this.selectFiles = const [],
|
||||
this.extra,
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ class Options {
|
||||
@JsonSerializable()
|
||||
class OptsExtraHttp {
|
||||
int connections = 0;
|
||||
bool autoTorrent = false;
|
||||
|
||||
OptsExtraHttp();
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ part of 'options.dart';
|
||||
Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
|
||||
name: json['name'] as String,
|
||||
path: json['path'] as String,
|
||||
selectFiles:
|
||||
(json['selectFiles'] as List<dynamic>).map((e) => e as int).toList(),
|
||||
selectFiles: (json['selectFiles'] as List<dynamic>?)
|
||||
?.map((e) => e as int)
|
||||
.toList() ??
|
||||
const [],
|
||||
extra: json['extra'],
|
||||
);
|
||||
|
||||
@@ -32,9 +34,12 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
|
||||
}
|
||||
|
||||
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
|
||||
OptsExtraHttp()..connections = json['connections'] as int;
|
||||
OptsExtraHttp()
|
||||
..connections = json['connections'] as int
|
||||
..autoTorrent = json['autoTorrent'] as bool;
|
||||
|
||||
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
|
||||
<String, dynamic>{
|
||||
'connections': instance.connections,
|
||||
'autoTorrent': instance.autoTorrent,
|
||||
};
|
||||
@@ -13,6 +13,7 @@ class Task {
|
||||
Status status;
|
||||
Progress progress;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
|
||||
Task({
|
||||
required this.id,
|
||||
@@ -20,6 +21,7 @@ class Task {
|
||||
required this.status,
|
||||
required this.progress,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
|
||||
|
||||
@@ -12,6 +12,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
@@ -20,6 +21,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'status': _$StatusEnumMap[instance.status]!,
|
||||
'progress': instance.progress.toJson(),
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
'updatedAt': instance.updatedAt.toIso8601String(),
|
||||
};
|
||||
|
||||
const _$StatusEnumMap = {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ApiProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = 'YOUR-API-URL';
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/database/entity.dart';
|
||||
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:uri_to_file/uri_to_file.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -16,17 +17,14 @@ import 'package:window_manager/window_manager.dart';
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../core/common/start_config.dart';
|
||||
import '../../../../generated/locales.g.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/log_util.dart';
|
||||
import '../../../../util/package_info.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
|
||||
const _startConfigNetwork = "start.network";
|
||||
const _startConfigAddress = "start.address";
|
||||
const _startConfigApiToken = "start.apiToken";
|
||||
|
||||
const unixSocketPath = 'gopeed.sock';
|
||||
|
||||
const allTrackerSubscribeUrls = [
|
||||
@@ -49,7 +47,12 @@ const allTrackerCdns = [
|
||||
[
|
||||
"https://ghproxy.com/https://raw.githubusercontent.com",
|
||||
r".*github.com(/.*)/raw(/.*)"
|
||||
]
|
||||
],
|
||||
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
[
|
||||
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
|
||||
r".*github.com(/.*)/raw(/.*)"
|
||||
],
|
||||
];
|
||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||
key: (v) => v as String,
|
||||
@@ -92,6 +95,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
_initForegroundTask().onError((error, stackTrace) =>
|
||||
logger.w("initForegroundTask error", error, stackTrace));
|
||||
|
||||
_initTrackerUpdate().onError((error, stackTrace) =>
|
||||
logger.w("initTrackerUpdate error", error, stackTrace));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -118,6 +124,27 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
Database.instance.saveWindowState(WindowStateEntity(isMaximized: true));
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowUnmaximize() {
|
||||
Database.instance.saveWindowState(WindowStateEntity(isMaximized: false));
|
||||
}
|
||||
|
||||
final _windowsResizeSave = Util.debounce(() async {
|
||||
final size = await windowManager.getSize();
|
||||
Database.instance.saveWindowState(
|
||||
WindowStateEntity(width: size.width, height: size.height));
|
||||
}, 500);
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
_windowsResizeSave();
|
||||
}
|
||||
|
||||
Future<void> _initDeepLinks() async {
|
||||
// currently only support android
|
||||
if (!Util.isAndroid()) {
|
||||
@@ -185,9 +212,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
MenuItem(
|
||||
label: 'donate'.tr,
|
||||
onClick: (menuItem) => {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
"https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate"),
|
||||
launchUrl(Uri.parse("https://docs.gopeed.com/donate.html"),
|
||||
mode: LaunchMode.externalApplication)
|
||||
},
|
||||
),
|
||||
@@ -209,6 +234,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<void> _initForegroundTask() async {
|
||||
if (!Util.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'gopeed_service',
|
||||
@@ -245,7 +274,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<void> _toCreate(Uri uri) async {
|
||||
final path = uri.scheme == "magnet"
|
||||
final path = (uri.scheme == "magnet" || uri.scheme == "http" || uri.scheme == "https")
|
||||
? uri.toString()
|
||||
: (await toFile(uri.toString())).path;
|
||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
|
||||
@@ -269,13 +298,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
_defaultStartConfig!.address = "127.0.0.1:0";
|
||||
} else {
|
||||
_defaultStartConfig!.network = "unix";
|
||||
if (Util.isDesktop()) {
|
||||
_defaultStartConfig!.address = unixSocketPath;
|
||||
}
|
||||
if (Util.isMobile()) {
|
||||
_defaultStartConfig!.address =
|
||||
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
|
||||
}
|
||||
_defaultStartConfig!.address =
|
||||
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
|
||||
}
|
||||
_defaultStartConfig!.apiToken = '';
|
||||
return _defaultStartConfig!;
|
||||
@@ -283,13 +307,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
Future<void> loadStartConfig() async {
|
||||
final defaultCfg = await _initDefaultStartConfig();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
startConfig.value.network =
|
||||
prefs.getString(_startConfigNetwork) ?? defaultCfg.network;
|
||||
startConfig.value.address =
|
||||
prefs.getString(_startConfigAddress) ?? defaultCfg.address;
|
||||
startConfig.value.apiToken =
|
||||
prefs.getString(_startConfigApiToken) ?? defaultCfg.apiToken;
|
||||
final saveCfg = Database.instance.getStartConfig();
|
||||
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
|
||||
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
|
||||
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
|
||||
}
|
||||
|
||||
Future<void> loadDownloaderConfig() async {
|
||||
@@ -316,6 +337,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
result.addAll(trackers);
|
||||
} catch (e) {
|
||||
logger.w("subscribe trackers fail, url: $u", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
btExtConfig.subscribeTrackers.clear();
|
||||
@@ -338,12 +360,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
btConfig.trackers.toSet().toList();
|
||||
}
|
||||
|
||||
Future<void> trackerUpdateOnStart() async {
|
||||
Future<void> _initTrackerUpdate() async {
|
||||
final btExtConfig = downloaderConfig.value.extra.bt;
|
||||
final lastUpdateTime = btExtConfig.lastTrackerUpdateTime;
|
||||
// if last update time is null or more than 1 day, update trackers
|
||||
if (lastUpdateTime == null ||
|
||||
lastUpdateTime.difference(DateTime.now()).inDays < 0) {
|
||||
if (btExtConfig.autoUpdateTrackers &&
|
||||
(lastUpdateTime == null ||
|
||||
lastUpdateTime.difference(DateTime.now()).inDays < 0)) {
|
||||
try {
|
||||
await trackerUpdate();
|
||||
} catch (e) {
|
||||
@@ -367,16 +390,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
_initDownloaderConfig() async {
|
||||
final config = downloaderConfig.value;
|
||||
if (config.protocolConfig.http.connections == 0) {
|
||||
config.protocolConfig.http.connections = 16;
|
||||
}
|
||||
final extra = config.extra;
|
||||
if (extra.themeMode.isEmpty) {
|
||||
extra.themeMode = ThemeMode.dark.name;
|
||||
}
|
||||
if (extra.locale.isEmpty) {
|
||||
final systemLocale = getLocaleKey(PlatformDispatcher.instance.locale);
|
||||
extra.locale = AppTranslation.translations.containsKey(systemLocale)
|
||||
extra.locale = messages.keys.containsKey(systemLocale)
|
||||
? systemLocale
|
||||
: getLocaleKey(fallbackLocale);
|
||||
}
|
||||
@@ -384,14 +404,21 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
// default select all tracker subscribe urls
|
||||
extra.bt.trackerSubscribeUrls.addAll(allTrackerSubscribeUrls);
|
||||
}
|
||||
final proxy = config.proxy;
|
||||
if (proxy.scheme.isEmpty) {
|
||||
proxy.scheme = 'http';
|
||||
}
|
||||
|
||||
if (config.downloadDir.isEmpty) {
|
||||
if (Util.isDesktop()) {
|
||||
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
|
||||
} else if (Util.isAndroid()) {
|
||||
config.downloadDir = (await getExternalStorageDirectory())?.path ??
|
||||
(await getApplicationDocumentsDirectory()).path;
|
||||
return;
|
||||
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
|
||||
if (downloadDir != null) {
|
||||
config.downloadDir = '$downloadDir/Gopeed';
|
||||
} else {
|
||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||
}
|
||||
} else if (Util.isIOS()) {
|
||||
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
|
||||
} else {
|
||||
@@ -401,10 +428,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<void> saveConfig() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_startConfigNetwork, startConfig.value.network);
|
||||
await prefs.setString(_startConfigAddress, startConfig.value.address);
|
||||
await prefs.setString(_startConfigApiToken, startConfig.value.apiToken);
|
||||
Database.instance.saveStartConfig(StartConfigEntity(
|
||||
network: startConfig.value.network,
|
||||
address: startConfig.value.address,
|
||||
apiToken: startConfig.value.apiToken));
|
||||
await putConfig(downloaderConfig.value);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../generated/locales.g.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../theme/theme.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
@@ -14,24 +15,24 @@ class AppView extends GetView<AppController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = controller.downloaderConfig.value;
|
||||
return GetMaterialApp.router(
|
||||
useInheritedMediaQuery: true,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: GopeedTheme.light,
|
||||
darkTheme: GopeedTheme.dark,
|
||||
themeMode: ThemeMode.values.byName(config.extra.themeMode),
|
||||
// translations: messages,
|
||||
translationsKeys: AppTranslation.translations,
|
||||
locale: toLocale(config.extra.locale),
|
||||
fallbackLocale: fallbackLocale,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales:
|
||||
AppTranslation.translations.keys.map((e) => toLocale(e)).toList(),
|
||||
getPages: AppPages.routes,
|
||||
return WithForegroundTask(
|
||||
child: GetMaterialApp.router(
|
||||
useInheritedMediaQuery: true,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: GopeedTheme.light,
|
||||
darkTheme: GopeedTheme.dark,
|
||||
themeMode: ThemeMode.values.byName(config.extra.themeMode),
|
||||
translations: messages,
|
||||
locale: toLocale(config.extra.locale),
|
||||
fallbackLocale: fallbackLocale,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
||||
getPages: AppPages.routes,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,15 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
|
||||
import 'package:gopeed/app/modules/history/views/history_view.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||
import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../api/model/resolve_result.dart';
|
||||
import '../../../../api/model/resource.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/util.dart';
|
||||
@@ -21,6 +20,7 @@ import '../../../routes/app_pages.dart';
|
||||
import '../../../views/directory_selector.dart';
|
||||
import '../../../views/file_list_view.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
import '../../history/views/history_view.dart';
|
||||
import '../controllers/create_controller.dart';
|
||||
|
||||
class CreateView extends GetView<CreateController> {
|
||||
@@ -35,7 +35,6 @@ class CreateView extends GetView<CreateController> {
|
||||
final _httpCookieController = TextEditingController();
|
||||
final _httpRefererController = TextEditingController();
|
||||
final _btTrackerController = TextEditingController();
|
||||
final _historyController = HistoryController();
|
||||
|
||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||
|
||||
@@ -117,7 +116,7 @@ class CreateView extends GetView<CreateController> {
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
autofocus: true,
|
||||
autofocus: !Util.isMobile(),
|
||||
controller: _urlController,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
@@ -168,10 +167,7 @@ class CreateView extends GetView<CreateController> {
|
||||
icon: const Icon(Icons.history_rounded),
|
||||
onPressed: () async {
|
||||
List<String> resultOfHistories =
|
||||
await _historyController.getAllHistory();
|
||||
// reversing: display last entered history first
|
||||
List<String> reverseResultOfHistories =
|
||||
resultOfHistories.reversed.toList();
|
||||
Database.instance.getCreateHistory() ?? [];
|
||||
// show dialog box to list history
|
||||
if (context.mounted) {
|
||||
showGeneralDialog(
|
||||
@@ -183,16 +179,14 @@ class CreateView extends GetView<CreateController> {
|
||||
opacity: a1.value,
|
||||
child: HistoryView(
|
||||
isHistoryListEmpty:
|
||||
reverseResultOfHistories.isEmpty,
|
||||
resultOfHistories.isEmpty,
|
||||
historyList: ListView.builder(
|
||||
itemCount:
|
||||
reverseResultOfHistories.length,
|
||||
itemCount: resultOfHistories.length,
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_urlController.text =
|
||||
reverseResultOfHistories[
|
||||
index];
|
||||
resultOfHistories[index];
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: MouseRegion(
|
||||
@@ -217,9 +211,7 @@ class CreateView extends GetView<CreateController> {
|
||||
10.0),
|
||||
),
|
||||
child: Text(
|
||||
reverseResultOfHistories[
|
||||
index]
|
||||
.toString(),
|
||||
resultOfHistories[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -409,15 +401,20 @@ class CreateView extends GetView<CreateController> {
|
||||
try {
|
||||
_confirmController.start();
|
||||
if (_confirmFormKey.currentState!.validate()) {
|
||||
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
||||
final isWebFileChosen =
|
||||
Util.isWeb() && controller.fileDataUri.isNotEmpty;
|
||||
final submitUrl = isWebFileChosen
|
||||
? controller.fileDataUri.value
|
||||
: _urlController.text;
|
||||
|
||||
final urls = Util.textToLines(submitUrl);
|
||||
// Add url to the history
|
||||
for (final url in urls) {
|
||||
_historyController.addHistory(url);
|
||||
if (!isWebFileChosen) {
|
||||
for (final url in urls) {
|
||||
Database.instance.saveCreateHistory(url);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Check if is direct download, there has two ways to direct download
|
||||
1. Direct download option is checked
|
||||
@@ -433,7 +430,7 @@ class CreateView extends GetView<CreateController> {
|
||||
name: isMultiLine ? "" : _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles: [],
|
||||
extra: parseReqOpts())));
|
||||
extra: parseReqOptsExtra())));
|
||||
}));
|
||||
Get.rootDelegate.offNamed(Routes.TASK);
|
||||
} else {
|
||||
@@ -471,11 +468,10 @@ class CreateView extends GetView<CreateController> {
|
||||
return reqExtra;
|
||||
}
|
||||
|
||||
Object? parseReqOpts() {
|
||||
return _connectionsController.text.isEmpty
|
||||
? null
|
||||
: (OptsExtraHttp()
|
||||
..connections = int.parse(_connectionsController.text));
|
||||
Object? parseReqOptsExtra() {
|
||||
return OptsExtraHttp()
|
||||
..connections = int.tryParse(_connectionsController.text) ?? 0
|
||||
..autoTorrent = true;
|
||||
}
|
||||
|
||||
String _hitText() {
|
||||
@@ -541,7 +537,7 @@ class CreateView extends GetView<CreateController> {
|
||||
showMessage('tip'.tr, 'noFileSelected'.tr);
|
||||
return;
|
||||
}
|
||||
final optExtra = parseReqOpts();
|
||||
final optExtra = parseReqOptsExtra();
|
||||
if (createFormKey.currentState!.validate()) {
|
||||
if (rr.id.isEmpty) {
|
||||
// from extension resolve result
|
||||
@@ -552,8 +548,8 @@ class CreateView extends GetView<CreateController> {
|
||||
req: file.req!,
|
||||
opt: Options(
|
||||
name: file.name,
|
||||
path: path.join(
|
||||
_pathController.text, rr.res.name),
|
||||
path: path.join(_pathController.text,
|
||||
rr.res.name, file.path),
|
||||
selectFiles: [],
|
||||
extra: optExtra)));
|
||||
}));
|
||||
|
||||
@@ -7,21 +7,21 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/views/text_button_loading.dart';
|
||||
import '../../../../api/model/update_extension_settings.dart';
|
||||
import '../../../../util/mac_secure_util.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||
import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/extension.dart';
|
||||
import '../../../../api/model/install_extension.dart';
|
||||
import '../../../../api/model/switch_extension.dart';
|
||||
import '../../../../api/model/update_extension_settings.dart';
|
||||
import '../../../../util/mac_secure_util.dart';
|
||||
import '../../../../util/message.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/icon_button_loading.dart';
|
||||
import '../../../views/responsive_builder.dart';
|
||||
import '../../../views/text_button_loading.dart';
|
||||
import '../controllers/extension_controller.dart';
|
||||
|
||||
class ExtensionView extends GetView<ExtensionController> {
|
||||
@@ -41,10 +41,8 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
'https://github.com/search?q=topic%3Agopeed-extension&type=repositories'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
launchUrl(Uri.parse(
|
||||
'https://github.com/search?q=topic%3Agopeed-extension&type=repositories'));
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
label: Text('extensionFind'.tr),
|
||||
@@ -53,8 +51,7 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse('https://docs.gopeed.com/dev-extension.html'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
Uri.parse('https://docs.gopeed.com/dev-extension.html'));
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
label: Text('extensionDevelop'.tr),
|
||||
@@ -208,20 +205,15 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse(extension.homepage),
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
Uri.parse(extension.homepage));
|
||||
},
|
||||
icon: const Icon(Icons.home))
|
||||
: null,
|
||||
extension.repository?.url.isNotEmpty == true
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
extension.repository!.url),
|
||||
mode: LaunchMode
|
||||
.externalApplication);
|
||||
launchUrl(Uri.parse(
|
||||
extension.repository!.url));
|
||||
},
|
||||
icon: const Icon(Icons.code))
|
||||
: null,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class HistoryController {
|
||||
// Get History in List<String>
|
||||
Future<List<String>> getAllHistory() async {
|
||||
SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||
List<String>? historyList = preferences.getStringList('history');
|
||||
if(historyList == null) {
|
||||
return [];
|
||||
}
|
||||
return historyList;
|
||||
}
|
||||
|
||||
// Get Existing History & Add New History
|
||||
void addHistory(String historyText) async {
|
||||
SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||
List<String> existingHistoryData = await getAllHistory();
|
||||
existingHistoryData.add(historyText);
|
||||
preferences.setStringList('history', existingHistoryData);
|
||||
}
|
||||
|
||||
void clearHistory() async {
|
||||
SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||
preferences.remove('history');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
|
||||
import 'package:gopeed/database/database.dart';
|
||||
|
||||
class HistoryView extends StatefulWidget {
|
||||
const HistoryView({
|
||||
@@ -62,8 +62,7 @@ class _HistoryViewState extends State<HistoryView> {
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
HistoryController _controller = HistoryController();
|
||||
_controller.clearHistory();
|
||||
Database.instance.clearCreateHistory();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
tooltip: "clearHistory".tr,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../routes/app_pages.dart';
|
||||
@@ -12,7 +11,7 @@ class HomeView extends GetView<HomeController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
|
||||
switch (currentRoute?.location) {
|
||||
switch (currentRoute?.uri.path) {
|
||||
case Routes.EXTENSION:
|
||||
controller.currentIndex.value = 1;
|
||||
break;
|
||||
@@ -24,141 +23,139 @@ class HomeView extends GetView<HomeController> {
|
||||
break;
|
||||
}
|
||||
|
||||
return WithForegroundTask(
|
||||
child: Scaffold(
|
||||
// extendBody: true,
|
||||
body: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
!ResponsiveBuilder.isNarrow(context)
|
||||
? NavigationRail(
|
||||
extended: true,
|
||||
labelType: NavigationRailLabelType.none,
|
||||
minExtendedWidth: 170,
|
||||
groupAlignment: 0,
|
||||
// useIndicator: false,
|
||||
onDestinationSelected: (int index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.task),
|
||||
selectedIcon: const Icon(Icons.task),
|
||||
label: Text('task'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.extension),
|
||||
selectedIcon: const Icon(Icons.extension),
|
||||
label: Text('extensions'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.settings),
|
||||
selectedIcon: const Icon(Icons.settings),
|
||||
label: Text('setting'.tr),
|
||||
),
|
||||
],
|
||||
selectedIndex: controller.currentIndex.value,
|
||||
leading: const Icon(Icons.menu),
|
||||
// trailing: const Icon(Icons.info_outline),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: GetRouterOutlet(
|
||||
initialRoute: Routes.TASK,
|
||||
// anchorRoute: '/',
|
||||
// filterPages: (afterAnchor) {
|
||||
// logger.w(afterAnchor);
|
||||
// logger.w(afterAnchor.take(1));
|
||||
// return afterAnchor.take(1);
|
||||
// },
|
||||
))
|
||||
]),
|
||||
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
|
||||
? BottomNavigationBar(
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.task),
|
||||
label: 'task'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.extension),
|
||||
label: 'extensions'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
label: 'setting'.tr,
|
||||
),
|
||||
],
|
||||
currentIndex: controller.currentIndex.value,
|
||||
// selectedItemColor: Get.theme.highlightColor,
|
||||
onTap: (index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
)
|
||||
// StylishBottomBar(
|
||||
// option: AnimatedBarOptions(
|
||||
// iconSize: 32,
|
||||
// barAnimation: BarAnimation.blink,
|
||||
// iconStyle: IconStyle.Default,
|
||||
// opacity: 0.3,
|
||||
// ),
|
||||
// items: [
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.file_download),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloading'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.done),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloaded'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('setting'.tr)),
|
||||
// ],
|
||||
// // hasNotch: true,
|
||||
// currentIndex: controller.currentIndex.value,
|
||||
// onTap: (index) {
|
||||
// switch (index) {
|
||||
// case 0:
|
||||
// delegate.toNamed(Routes.DOWNLOADING);
|
||||
// controller.currentIndex.value = 0;
|
||||
// break;
|
||||
// case 1:
|
||||
// delegate.toNamed(Routes.DOWNLOADED);
|
||||
// controller.currentIndex.value = 1;
|
||||
// break;
|
||||
// case 2:
|
||||
// delegate.toNamed(Routes.SETTING);
|
||||
// controller.currentIndex.value = 2;
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
return Scaffold(
|
||||
// extendBody: true,
|
||||
body: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
!ResponsiveBuilder.isNarrow(context)
|
||||
? NavigationRail(
|
||||
extended: true,
|
||||
labelType: NavigationRailLabelType.none,
|
||||
minExtendedWidth: 170,
|
||||
groupAlignment: 0,
|
||||
// useIndicator: false,
|
||||
onDestinationSelected: (int index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.task),
|
||||
selectedIcon: const Icon(Icons.task),
|
||||
label: Text('task'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.extension),
|
||||
selectedIcon: const Icon(Icons.extension),
|
||||
label: Text('extensions'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.settings),
|
||||
selectedIcon: const Icon(Icons.settings),
|
||||
label: Text('setting'.tr),
|
||||
),
|
||||
],
|
||||
selectedIndex: controller.currentIndex.value,
|
||||
leading: const Icon(Icons.menu),
|
||||
// trailing: const Icon(Icons.info_outline),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: GetRouterOutlet(
|
||||
initialRoute: Routes.TASK,
|
||||
// anchorRoute: '/',
|
||||
// filterPages: (afterAnchor) {
|
||||
// logger.w(afterAnchor);
|
||||
// logger.w(afterAnchor.take(1));
|
||||
// return afterAnchor.take(1);
|
||||
// },
|
||||
))
|
||||
]),
|
||||
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
|
||||
? BottomNavigationBar(
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.task),
|
||||
label: 'task'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.extension),
|
||||
label: 'extensions'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
label: 'setting'.tr,
|
||||
),
|
||||
],
|
||||
currentIndex: controller.currentIndex.value,
|
||||
// selectedItemColor: Get.theme.highlightColor,
|
||||
onTap: (index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
)
|
||||
// StylishBottomBar(
|
||||
// option: AnimatedBarOptions(
|
||||
// iconSize: 32,
|
||||
// barAnimation: BarAnimation.blink,
|
||||
// iconStyle: IconStyle.Default,
|
||||
// opacity: 0.3,
|
||||
// ),
|
||||
// items: [
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.file_download),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloading'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.done),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloaded'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('setting'.tr)),
|
||||
// ],
|
||||
// // hasNotch: true,
|
||||
// currentIndex: controller.currentIndex.value,
|
||||
// onTap: (index) {
|
||||
// switch (index) {
|
||||
// case 0:
|
||||
// delegate.toNamed(Routes.DOWNLOADING);
|
||||
// controller.currentIndex.value = 0;
|
||||
// break;
|
||||
// case 1:
|
||||
// delegate.toNamed(Routes.DOWNLOADED);
|
||||
// controller.currentIndex.value = 1;
|
||||
// break;
|
||||
// case 2:
|
||||
// delegate.toNamed(Routes.SETTING);
|
||||
// controller.currentIndex.value = 2;
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../generated/locales.g.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
import '../../../../util/message.dart';
|
||||
@@ -15,6 +15,7 @@ import '../../../../util/package_info.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/check_list_view.dart';
|
||||
import '../../../views/directory_selector.dart';
|
||||
import '../../../views/open_in_new.dart';
|
||||
import '../../../views/outlined_button_loading.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
import '../controllers/setting_controller.dart';
|
||||
@@ -93,9 +94,36 @@ class SettingView extends GetView<SettingController> {
|
||||
],
|
||||
);
|
||||
});
|
||||
buildBrowserExtension() {
|
||||
return ListTile(
|
||||
title: Text('browserExtension'.tr),
|
||||
subtitle: const OpenInNew(
|
||||
text: "Chrome",
|
||||
url:
|
||||
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
|
||||
));
|
||||
}
|
||||
|
||||
// http config items start
|
||||
final httpConfig = downloaderCfg.value.protocolConfig.http;
|
||||
final buildHttpUa =
|
||||
_buildConfigItem('User-Agent', () => httpConfig.userAgent, (Key key) {
|
||||
final uaController = TextEditingController(text: httpConfig.userAgent);
|
||||
uaController.addListener(() async {
|
||||
if (uaController.text.isNotEmpty &&
|
||||
uaController.text != httpConfig.userAgent) {
|
||||
httpConfig.userAgent = uaController.text;
|
||||
|
||||
await debounceSave();
|
||||
}
|
||||
});
|
||||
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
controller: uaController,
|
||||
);
|
||||
});
|
||||
final buildHttpConnections = _buildConfigItem(
|
||||
'connections', () => httpConfig.connections.toString(), (Key key) {
|
||||
final connectionsController =
|
||||
@@ -120,10 +148,49 @@ class SettingView extends GetView<SettingController> {
|
||||
],
|
||||
);
|
||||
});
|
||||
final buildHttpUseServerCtime = _buildConfigItem('useServerCtime'.tr,
|
||||
() => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr, (Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value: httpConfig.useServerCtime,
|
||||
onChanged: (bool value) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.protocolConfig.http.useServerCtime = value;
|
||||
});
|
||||
debounceSave();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// bt config items start
|
||||
final btConfig = downloaderCfg.value.protocolConfig.bt;
|
||||
final btExtConfig = downloaderCfg.value.extra.bt;
|
||||
final buildBtListenPort = _buildConfigItem(
|
||||
'port', () => btConfig.listenPort.toString(), (Key key) {
|
||||
final listenPortController =
|
||||
TextEditingController(text: btConfig.listenPort.toString());
|
||||
listenPortController.addListener(() async {
|
||||
if (listenPortController.text.isNotEmpty &&
|
||||
listenPortController.text != btConfig.listenPort.toString()) {
|
||||
btConfig.listenPort = int.parse(listenPortController.text);
|
||||
|
||||
await debounceSave();
|
||||
}
|
||||
});
|
||||
|
||||
return TextField(
|
||||
key: key,
|
||||
focusNode: FocusNode(),
|
||||
controller: listenPortController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 0, max: 65535),
|
||||
],
|
||||
);
|
||||
});
|
||||
final buildBtTrackerSubscribeUrls = _buildConfigItem(
|
||||
'subscribeTracker',
|
||||
() => 'items'.trParams(
|
||||
@@ -165,8 +232,13 @@ class SettingView extends GetView<SettingController> {
|
||||
Expanded(
|
||||
child: SwitchListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: true,
|
||||
onChanged: (bool value) {},
|
||||
value: btExtConfig.autoUpdateTrackers,
|
||||
onChanged: (bool value) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.extra.bt.autoUpdateTrackers = value;
|
||||
});
|
||||
debounceSave();
|
||||
},
|
||||
title: Text('updateDaily'.tr)),
|
||||
),
|
||||
],
|
||||
@@ -230,8 +302,7 @@ class SettingView extends GetView<SettingController> {
|
||||
));
|
||||
final buildLocale = _buildConfigItem(
|
||||
'locale',
|
||||
() => AppTranslation
|
||||
.translations[downloaderCfg.value.extra.locale]!['label']!,
|
||||
() => messages.keys[downloaderCfg.value.extra.locale]!['label']!,
|
||||
(Key key) => DropdownButton<String>(
|
||||
key: key,
|
||||
value: downloaderCfg.value.extra.locale,
|
||||
@@ -245,23 +316,26 @@ class SettingView extends GetView<SettingController> {
|
||||
|
||||
await debounceSave();
|
||||
},
|
||||
items: AppTranslation.translations.keys
|
||||
items: messages.keys.keys
|
||||
.map((e) => DropdownMenuItem<String>(
|
||||
value: e,
|
||||
child: Text(AppTranslation.translations[e]!['label']!),
|
||||
child: Text(messages.keys[e]!['label']!),
|
||||
))
|
||||
.toList(),
|
||||
));
|
||||
|
||||
// about config items start
|
||||
buildHomepage() => ListTile(
|
||||
title: Text('homepage'.tr),
|
||||
subtitle: const Text('https://github.com/GopeedLab/gopeed'),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse('https://github.com/GopeedLab/gopeed'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
},
|
||||
);
|
||||
buildHomepage() {
|
||||
const homePage = 'https://gopeed.com';
|
||||
return ListTile(
|
||||
title: Text('homepage'.tr),
|
||||
subtitle: const Text(homePage),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse(homePage), mode: LaunchMode.externalApplication);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
buildVersion() {
|
||||
bool isNewVersion(String current, String latest) {
|
||||
if (latest == "") {
|
||||
@@ -321,7 +395,160 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
}
|
||||
|
||||
// advanced config items start
|
||||
buildThanks() {
|
||||
const thankPage =
|
||||
'https://github.com/GopeedLab/gopeed/graphs/contributors';
|
||||
return ListTile(
|
||||
title: Text('thanks'.tr),
|
||||
subtitle: Text('thanksDesc'.tr),
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse(thankPage), mode: LaunchMode.externalApplication);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// advanced config proxy items start
|
||||
final buildProxy = _buildConfigItem(
|
||||
'proxy',
|
||||
() => downloaderCfg.value.proxy.enable
|
||||
? '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}'
|
||||
: 'notSet'.tr,
|
||||
(Key key) {
|
||||
final proxy = downloaderCfg.value.proxy;
|
||||
|
||||
final switcher = Switch(
|
||||
value: proxy.enable,
|
||||
onChanged: (bool value) async {
|
||||
if (value != proxy.enable) {
|
||||
downloaderCfg.update((val) {
|
||||
val!.proxy.enable = value;
|
||||
});
|
||||
|
||||
await debounceSave();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final scheme = SizedBox(
|
||||
width: 100,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: proxy.scheme,
|
||||
onChanged: (value) async {
|
||||
if (value != null && value != proxy.scheme) {
|
||||
proxy.scheme = value;
|
||||
|
||||
await debounceSave();
|
||||
}
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem<String>(
|
||||
value: 'http',
|
||||
child: Text('HTTP'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'https',
|
||||
child: Text('HTTPS'),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: 'socks5',
|
||||
child: Text('SOCKS5'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final arr = proxy.host.split(':');
|
||||
var host = '';
|
||||
var port = '';
|
||||
if (arr.length > 1) {
|
||||
host = arr[0];
|
||||
port = arr[1];
|
||||
}
|
||||
|
||||
final ipController = TextEditingController(text: host);
|
||||
final portController = TextEditingController(text: port);
|
||||
updateAddress() async {
|
||||
final newAddress = '${ipController.text}:${portController.text}';
|
||||
if (newAddress != startCfg.value.address) {
|
||||
proxy.host = newAddress;
|
||||
|
||||
await debounceSave();
|
||||
}
|
||||
}
|
||||
|
||||
ipController.addListener(updateAddress);
|
||||
portController.addListener(updateAddress);
|
||||
final server = [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: ipController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'server'.tr,
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 10)),
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: portController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'port'.tr,
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
NumericalRangeFormatter(min: 0, max: 65535),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final usrController = TextEditingController(text: proxy.usr);
|
||||
final pwdController = TextEditingController(text: proxy.pwd);
|
||||
|
||||
final auth = [
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: usrController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'username'.tr,
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 10)),
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: pwdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'password'.tr,
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return Form(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _addPadding([
|
||||
switcher,
|
||||
scheme,
|
||||
Row(
|
||||
children: server,
|
||||
),
|
||||
Row(
|
||||
children: auth,
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// advanced config API items start
|
||||
final buildApiProtocol = _buildConfigItem(
|
||||
'protocol',
|
||||
() => startCfg.value.network == 'tcp'
|
||||
@@ -330,7 +557,7 @@ class SettingView extends GetView<SettingController> {
|
||||
(Key key) {
|
||||
final items = <Widget>[
|
||||
SizedBox(
|
||||
width: 150,
|
||||
width: 80,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: startCfg.value.network,
|
||||
onChanged: Util.isDesktop()
|
||||
@@ -381,8 +608,7 @@ class SettingView extends GetView<SettingController> {
|
||||
portController.addListener(updateAddress);
|
||||
items.addAll([
|
||||
const Padding(padding: EdgeInsets.only(left: 20)),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: ipController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -396,8 +622,7 @@ class SettingView extends GetView<SettingController> {
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 10)),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
Flexible(
|
||||
child: TextFormField(
|
||||
controller: portController,
|
||||
decoration: InputDecoration(
|
||||
@@ -472,20 +697,26 @@ class SettingView extends GetView<SettingController> {
|
||||
Text('general'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider(
|
||||
[buildDownloadDir(), buildMaxRunning()]),
|
||||
children: _addDivider([
|
||||
buildDownloadDir(),
|
||||
buildMaxRunning(),
|
||||
buildBrowserExtension()
|
||||
]),
|
||||
)),
|
||||
const Text('HTTP'),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildHttpUa(),
|
||||
buildHttpConnections(),
|
||||
buildHttpUseServerCtime(),
|
||||
]),
|
||||
)),
|
||||
const Text('BitTorrent'),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildBtListenPort(),
|
||||
buildBtTrackerSubscribeUrls(),
|
||||
buildBtTrackers(),
|
||||
]),
|
||||
@@ -504,26 +735,48 @@ class SettingView extends GetView<SettingController> {
|
||||
children: _addDivider([
|
||||
buildHomepage(),
|
||||
buildVersion(),
|
||||
buildThanks(),
|
||||
]),
|
||||
)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
// Column(
|
||||
// children: [
|
||||
// Card(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// ..._addDivider([
|
||||
// buildApiProtocol(),
|
||||
// Util.isDesktop() && startCfg.value.network == 'tcp'
|
||||
// ? buildApiToken()
|
||||
// : null,
|
||||
// ]),
|
||||
// ],
|
||||
// )),
|
||||
// ],
|
||||
// ),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _addPadding([
|
||||
Text('network'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
..._addDivider([
|
||||
buildApiProtocol(),
|
||||
Util.isDesktop() && startCfg.value.network == 'tcp'
|
||||
? buildApiToken()
|
||||
: null,
|
||||
]),
|
||||
],
|
||||
children: _addDivider([buildProxy()]),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const Text('API'),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([
|
||||
buildApiProtocol(),
|
||||
Util.isDesktop() && startCfg.value.network == 'tcp'
|
||||
? buildApiToken()
|
||||
: null,
|
||||
]),
|
||||
)),
|
||||
]),
|
||||
))
|
||||
],
|
||||
).paddingOnly(left: 16, right: 16, top: 16, bottom: 16)),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
|
||||
|
||||
import '../../../../api/model/task.dart';
|
||||
|
||||
class TaskDownloadedController extends TaskListController {
|
||||
TaskDownloadedController() : super([Status.done]);
|
||||
TaskDownloadedController()
|
||||
: super([Status.done], (a, b) => b.updatedAt.compareTo(a.updatedAt));
|
||||
}
|
||||
@@ -9,5 +9,5 @@ class TaskDownloadingController extends TaskListController {
|
||||
Status.pause,
|
||||
Status.wait,
|
||||
Status.error
|
||||
]);
|
||||
], (a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import '../../../../api/model/task.dart';
|
||||
|
||||
abstract class TaskListController extends GetxController {
|
||||
List<Status> statuses;
|
||||
int Function(Task a, Task b) compare;
|
||||
|
||||
TaskListController(this.statuses);
|
||||
TaskListController(this.statuses, this.compare);
|
||||
|
||||
final tasks = <Task>[].obs;
|
||||
final isRunning = false.obs;
|
||||
@@ -43,6 +44,9 @@ abstract class TaskListController extends GetxController {
|
||||
}
|
||||
|
||||
getTasksState() async {
|
||||
tasks.value = await getTasks(statuses);
|
||||
final tasks = await getTasks(statuses);
|
||||
// sort tasks by create time
|
||||
tasks.sort(compare);
|
||||
this.tasks.value = tasks;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/api.dart' as api;
|
||||
import '../../../../util/browser_download/browser_download.dart';
|
||||
import '../../../../util/file_icon.dart';
|
||||
import '../../../../util/icons.dart';
|
||||
import '../../../../util/util.dart';
|
||||
@@ -21,7 +24,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.rootDelegate.popRoute()),
|
||||
// actions: [],
|
||||
title: Obx(() => Text(controller.task.value!.meta.res!.name)),
|
||||
title: Obx(() => Text(controller.task.value?.meta.res?.name ?? "")),
|
||||
),
|
||||
body: Obx(() {
|
||||
final fileList = controller.fileList;
|
||||
@@ -53,16 +56,18 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
itemBuilder: (context, index) {
|
||||
final meta = controller.task.value!.meta;
|
||||
final file = fileList[index];
|
||||
final filePath = Util.safePathJoin([
|
||||
meta.opts.path,
|
||||
meta.res!.name,
|
||||
file.filePath(meta.opts.name)
|
||||
]);
|
||||
// if resource is single file, use opts.name as file name
|
||||
final realFileName = meta.res!.name.isEmpty
|
||||
? (meta.opts.name.isEmpty ? file.name : meta.opts.name)
|
||||
: "";
|
||||
final fileRelativePath = file.filePath(realFileName);
|
||||
final filePath = Util.safePathJoin(
|
||||
[meta.opts.path, meta.res!.name, fileRelativePath]);
|
||||
final fileName = basename(filePath);
|
||||
return ListTile(
|
||||
leading: file.isDirectory
|
||||
? const Icon(Icons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(file.name)]),
|
||||
: Icon(FaIcons.allIcons[findIcon(fileName)]),
|
||||
title: Text(fileName),
|
||||
subtitle: file.isDirectory
|
||||
? Text('items'.trParams({
|
||||
@@ -77,19 +82,44 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
width: 100,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_circle),
|
||||
onPressed: () {
|
||||
OpenFile.open(filePath);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () {
|
||||
final xfile = XFile(filePath);
|
||||
Share.shareXFiles([xfile]);
|
||||
})
|
||||
],
|
||||
children: Util.isWeb()
|
||||
? () {
|
||||
final accessUrl = api.join(
|
||||
"/fs/tasks/${controller.task.value!.id}$fileRelativePath");
|
||||
return [
|
||||
IconButton(
|
||||
icon:
|
||||
const Icon(Icons.play_circle),
|
||||
onPressed: () {
|
||||
launchUrl(Uri.parse(accessUrl),
|
||||
webOnlyWindowName:
|
||||
"_blank");
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () {
|
||||
download(accessUrl, fileName);
|
||||
})
|
||||
];
|
||||
}()
|
||||
: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.play_circle),
|
||||
onPressed: () async {
|
||||
print(filePath);
|
||||
try {
|
||||
await OpenFilex.open(filePath);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () {
|
||||
final xfile = XFile(filePath);
|
||||
Share.shareXFiles([xfile]);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/app/modules/task/controllers/task_downloading_controller.dart';
|
||||
import '../controllers/task_downloaded_controller.dart';
|
||||
import 'task_downloading_view.dart';
|
||||
|
||||
import 'task_downloaded_view.dart';
|
||||
import '../controllers/task_controller.dart';
|
||||
import '../controllers/task_downloaded_controller.dart';
|
||||
import '../controllers/task_downloading_controller.dart';
|
||||
import 'task_downloaded_view.dart';
|
||||
import 'task_downloading_view.dart';
|
||||
|
||||
class TaskView extends GetView<TaskController> {
|
||||
const TaskView({Key? key}) : super(key: key);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gopeed/api/model/meta.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import '../../api/api.dart';
|
||||
import '../../api/model/meta.dart';
|
||||
import '../../api/model/task.dart';
|
||||
import '../../util/file_explorer.dart';
|
||||
import '../../util/file_icon.dart';
|
||||
@@ -110,19 +110,17 @@ class BuildTaskListView extends GetView {
|
||||
List<Widget> buildActions() {
|
||||
final list = <Widget>[];
|
||||
if (isDone()) {
|
||||
if (Util.isDesktop() || Util.isMobile()) {
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () {
|
||||
if (Util.isDesktop()) {
|
||||
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
|
||||
} else {
|
||||
Get.rootDelegate
|
||||
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
list.add(IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () {
|
||||
if (Util.isDesktop()) {
|
||||
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
|
||||
} else {
|
||||
Get.rootDelegate
|
||||
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
|
||||
}
|
||||
},
|
||||
));
|
||||
} else {
|
||||
if (isRunning()) {
|
||||
list.add(IconButton(
|
||||
@@ -207,9 +205,11 @@ class BuildTaskListView extends GetView {
|
||||
)),
|
||||
],
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
value: getProgress(),
|
||||
),
|
||||
isDone()
|
||||
? Container()
|
||||
: LinearProgressIndicator(
|
||||
value: getProgress(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)).padding(horizontal: 14, top: 8);
|
||||
|
||||
@@ -35,7 +35,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
|
||||
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
|
||||
},
|
||||
)),
|
||||
!Util.isWeb() && !Util.isIOS()
|
||||
!Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OpenInNew extends StatelessWidget {
|
||||
final String text;
|
||||
final String url;
|
||||
|
||||
const OpenInNew({super.key, required this.text, required this.url});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Get.theme.colorScheme.background,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize
|
||||
.min, // Set the row's size to be as small as possible
|
||||
children: <Widget>[
|
||||
Text(text),
|
||||
const SizedBox(
|
||||
width: 4), // Add some space between the text and the icon
|
||||
const Icon(
|
||||
Icons.open_in_new,
|
||||
size: 14,
|
||||
), // The icon is after the text
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import "libgopeed_boot_stub.dart"
|
||||
if (dart.library.io) 'entry/libgopeed_boot_native.dart';
|
||||
|
||||
abstract class LibgopeedBoot {
|
||||
static const unixSocketPath = 'gopeed.sock';
|
||||
|
||||
static LibgopeedBoot? _instance;
|
||||
|
||||
static LibgopeedBoot get instance {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:gopeed/util/util.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
import 'entity.dart';
|
||||
|
||||
const String _startConfig = 'startConfig';
|
||||
const String _lastRunningConfig = 'lastRunningConfig';
|
||||
const String _windowState = 'windowState';
|
||||
const String _bookmark = 'bookmark';
|
||||
const String _createHistory = 'createHistory';
|
||||
|
||||
class Database {
|
||||
static final Database _instance = Database._internal();
|
||||
|
||||
static Database get instance => _instance;
|
||||
|
||||
factory Database() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
late Box box;
|
||||
|
||||
Database._internal();
|
||||
|
||||
Future<void> init() async {
|
||||
Hive.init(Util.getStorageDir());
|
||||
box = await Hive.openBox('database');
|
||||
}
|
||||
|
||||
void save<T>(String key, T entity) {
|
||||
box.put(key, jsonEncode(entity));
|
||||
}
|
||||
|
||||
T? get<T>(String key, T Function(dynamic json) fromJsonT) {
|
||||
final json = box.get(key);
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return fromJsonT(jsonDecode(json));
|
||||
}
|
||||
|
||||
void clear(String key) {
|
||||
box.delete(key);
|
||||
}
|
||||
|
||||
void saveStartConfig(StartConfigEntity entity) {
|
||||
save<StartConfigEntity>(_startConfig, entity);
|
||||
}
|
||||
|
||||
StartConfigEntity? getStartConfig() {
|
||||
return get<StartConfigEntity>(
|
||||
_startConfig, (json) => StartConfigEntity.fromJson(json));
|
||||
}
|
||||
|
||||
void saveLastRunningConfig(StartConfigEntity entity) {
|
||||
save<StartConfigEntity>(_lastRunningConfig, entity);
|
||||
}
|
||||
|
||||
StartConfigEntity? getLastRunningConfig() {
|
||||
return get<StartConfigEntity>(
|
||||
_lastRunningConfig, (json) => StartConfigEntity.fromJson(json));
|
||||
}
|
||||
|
||||
/// Patch non-null fields with the original value
|
||||
void saveWindowState(WindowStateEntity entity) {
|
||||
final state = getWindowState();
|
||||
entity.isMaximized ??= state?.isMaximized;
|
||||
entity.width ??= state?.width;
|
||||
entity.height ??= state?.height;
|
||||
save<WindowStateEntity>(_windowState, entity);
|
||||
}
|
||||
|
||||
WindowStateEntity? getWindowState() {
|
||||
return get<WindowStateEntity>(
|
||||
_windowState, (json) => WindowStateEntity.fromJson(json));
|
||||
}
|
||||
|
||||
/// Use map to ensure that the same directory only saves the latest bookmark
|
||||
void saveBookmark(MapEntry<String, String> entry) {
|
||||
final map = getBookmark() ?? {};
|
||||
map[entry.key] = entry.value;
|
||||
save<Map<String, String>>(_bookmark, map);
|
||||
}
|
||||
|
||||
Map<String, String>? getBookmark() {
|
||||
return get<Map<String, String>>(_bookmark, (json) {
|
||||
return (json as Map<String, dynamic>)
|
||||
.map((key, value) => MapEntry(key, value.toString()));
|
||||
});
|
||||
}
|
||||
|
||||
void saveCreateHistory(String url) {
|
||||
var list = getCreateHistory() ?? [];
|
||||
list.remove(url);
|
||||
list.insert(0, url);
|
||||
if (list.length > 64) {
|
||||
list.removeLast();
|
||||
}
|
||||
save<List<String>>(_createHistory, list);
|
||||
}
|
||||
|
||||
List<String>? getCreateHistory() {
|
||||
return get<List<String>>(_createHistory, (json) {
|
||||
return (json as List<dynamic>).map((e) => e.toString()).toList();
|
||||
});
|
||||
}
|
||||
|
||||
void clearCreateHistory() {
|
||||
clear(_createHistory);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user