mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a628bb44a | ||
|
|
9955e482a4 | ||
|
|
27dc7aea52 | ||
|
|
14df9490cf | ||
|
|
431fdd0d54 | ||
|
|
25bf3ca65b |
No files matched your search
+1
-1
@@ -12,4 +12,4 @@
|
||||
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
# github: monkeyWie
|
||||
ko_fi: gopeed
|
||||
custom: https://gopeed.com/docs/donate
|
||||
custom: https://docs.gopeed.com/donate.html
|
||||
@@ -40,24 +40,16 @@ template: |
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="4">🪟 Windows</td>
|
||||
<td rowspan="2"><code>EXE</code></td>
|
||||
<td rowspan="2">🪟 Windows</td>
|
||||
<td><code>EXE</code></td>
|
||||
<td>amd64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>arm64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2"><code>Portable</code></td>
|
||||
<td><code>Portable</code></td>
|
||||
<td>amd64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>arm64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-arm64-portable.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">🍎 MacOS</td>
|
||||
<td rowspan="3"><code>DMG</code></td>
|
||||
|
||||
+41
-118
@@ -14,9 +14,7 @@ on:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.24"
|
||||
FLUTTER_VERSION: "3.41.2"
|
||||
GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }}
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -36,62 +34,19 @@ jobs:
|
||||
|
||||
build-windows:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
||||
runs-on: windows-latest
|
||||
needs: [get-release]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: arm64
|
||||
os: windows-11-arm
|
||||
llvm_ver: "20251202" # Only ARM64
|
||||
flutter_channel: "main"
|
||||
flutter_version: "7e1c8868"
|
||||
- arch: amd64
|
||||
os: windows-2022
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Enable long paths for flutter main branch checks
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
|
||||
# Install LLVM environment only on ARM64
|
||||
- name: Install llvm-mingw-ucrt-aarch64 (ARM64 only)
|
||||
if: matrix.arch == 'arm64'
|
||||
run: |
|
||||
$ver = "${{ matrix.llvm_ver }}"
|
||||
$url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$ver/llvm-mingw-$ver-ucrt-aarch64.zip"
|
||||
$zip = "$env:RUNNER_TEMP\\llvm.zip"
|
||||
$extract = "$env:RUNNER_TEMP\\extract"
|
||||
$target = "C:\\clangarm64"
|
||||
|
||||
curl -L $url -o $zip
|
||||
rm -r -fo $extract,$target -ea Ignore
|
||||
mkdir $extract | Out-Null
|
||||
|
||||
tar -xf $zip -C $extract
|
||||
mv (Get-ChildItem $extract)[0].FullName $target
|
||||
|
||||
$b = "$target\\bin"
|
||||
"CC=$b\\clang.exe" >> $env:GITHUB_ENV
|
||||
"CXX=$b\\clang++.exe" >> $env:GITHUB_ENV
|
||||
"CLANGARM64_BIN=$b" >> $env:GITHUB_ENV
|
||||
"CGO_ENABLED=1" >> $env:GITHUB_ENV
|
||||
"CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV
|
||||
$b >> $env:GITHUB_PATH
|
||||
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: ${{ matrix.flutter_channel || 'stable' }}
|
||||
flutter-version: ${{ matrix.flutter_version || env.FLUTTER_VERSION }}
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
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\"
|
||||
@@ -101,30 +56,21 @@ jobs:
|
||||
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater.exe github.com/GopeedLab/gopeed/cmd/updater
|
||||
cd ui/flutter
|
||||
$TAG = "v$env:VERSION"
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable"
|
||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||
$system = "C:\Windows\System32"
|
||||
if ("${{ matrix.arch }}" -eq "amd64") {
|
||||
# for amd64
|
||||
$release = "build\windows\x64\runner\Release\"
|
||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||
cp $mingw\libstdc++-6.dll $release
|
||||
cp $mingw\libgcc_s_seh-1.dll $release
|
||||
} else {
|
||||
# for ARM64
|
||||
$release = "build\windows\arm64\runner\Release\"
|
||||
$mingw = "$env:CLANGARM64_ROOT\aarch64-w64-mingw32\bin"
|
||||
cp $mingw\libc++.dll $release
|
||||
cp $mingw\libunwind.dll $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
|
||||
cp $system\msvcp140.dll $release
|
||||
cp $system\vcruntime140.dll $release
|
||||
cp $system\vcruntime140_1.dll $release
|
||||
|
||||
New-Item -Path build\windows\Output -ItemType Directory
|
||||
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-${{ matrix.arch }}-portable.zip"
|
||||
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
|
||||
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller"
|
||||
cd build/windows
|
||||
echo @"
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
@@ -170,7 +116,7 @@ jobs:
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: ".\${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }}\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]
|
||||
@@ -184,8 +130,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-$TAG-windows-${{ matrix.arch }}.exe"
|
||||
Compress-Archive -Path "Output\Gopeed-$TAG-windows-${{ matrix.arch }}.exe" -DestinationPath "Output\Gopeed-$TAG-windows-${{ matrix.arch }}.zip"
|
||||
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:
|
||||
@@ -194,7 +141,7 @@ jobs:
|
||||
overwrite: true
|
||||
build-macos-amd64-lib:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-15-intel
|
||||
runs-on: macos-13
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -220,7 +167,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
channel: [arm64, amd64, universal]
|
||||
channel: [ arm64, amd64, universal ]
|
||||
needs: [get-release, build-macos-amd64-lib]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -251,7 +198,7 @@ jobs:
|
||||
name: Build Amd64 Libraries
|
||||
run: |
|
||||
mkdir -p ui/flutter/macos/Frameworks
|
||||
|
||||
|
||||
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/libgopeed.dylib
|
||||
cp ui/flutter/lib-amd64/host ui/flutter/assets/exec/host
|
||||
cp ui/flutter/lib-amd64/updater ui/flutter/assets/exec/updater
|
||||
@@ -259,17 +206,17 @@ jobs:
|
||||
name: Build Universal Libraries
|
||||
run: |
|
||||
mkdir -p ui/flutter/macos/Frameworks
|
||||
|
||||
|
||||
# arm64 lib
|
||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/.temp/libgopeed.dylib.arm64 github.com/GopeedLab/gopeed/bind/desktop
|
||||
go build -ldflags="-w -s" -o ui/flutter/.temp/host.arm64 github.com/GopeedLab/gopeed/cmd/host
|
||||
go build -ldflags="-w -s" -o ui/flutter/.temp/updater.arm64 github.com/GopeedLab/gopeed/cmd/updater
|
||||
|
||||
|
||||
# amd64 lib
|
||||
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.amd64
|
||||
cp ui/flutter/lib-amd64/host ui/flutter/.temp/host.amd64
|
||||
cp ui/flutter/lib-amd64/updater ui/flutter/.temp/updater.amd64
|
||||
|
||||
|
||||
# universal lib
|
||||
lipo -create -output ui/flutter/macos/Frameworks/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.arm64 ui/flutter/.temp/libgopeed.dylib.amd64
|
||||
lipo -create -output ui/flutter/assets/exec/host ui/flutter/.temp/host.arm64 ui/flutter/.temp/host.amd64
|
||||
@@ -279,7 +226,7 @@ jobs:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||
cd build/macos/Build/Products/Release
|
||||
cat>appdmg.json<<EOF
|
||||
{
|
||||
@@ -293,7 +240,7 @@ jobs:
|
||||
EOF
|
||||
mkdir dist
|
||||
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
||||
|
||||
|
||||
if [[ "${{ matrix.channel }}" == "arm64" ]]; then
|
||||
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-arm64.dmg
|
||||
fi
|
||||
@@ -318,15 +265,24 @@ jobs:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
- if: matrix.os == 'ubuntu-22.04'
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- if: matrix.os == 'ubuntu-22.04-arm'
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: master
|
||||
- run: |
|
||||
sudo apt update -y
|
||||
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev rpm patchelf libfuse2 locate libkeybinder-3.0-dev
|
||||
# flutter build dependencies
|
||||
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
|
||||
# appimage build dependencies
|
||||
sudo apt install -y libfuse2 locate
|
||||
arch=$(uname -m)
|
||||
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||
chmod +x appimagetool
|
||||
sudo mv appimagetool /usr/local/bin/
|
||||
- name: Build
|
||||
@@ -338,22 +294,6 @@ jobs:
|
||||
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||
|
||||
cd ui/flutter
|
||||
|
||||
RPM_ARCH=$(uname -m)
|
||||
mkdir -p linux/packaging/rpm
|
||||
cat << EOF > linux/packaging/rpm/make_config.yaml
|
||||
display_name: Gopeed
|
||||
icon: assets/icon/icon.svg
|
||||
summary: High speed downloader.
|
||||
description: A high speed downloader that supports all platforms.
|
||||
group: Applications/Internet
|
||||
license: GPL-3.0
|
||||
url: https://github.com/GopeedLab/gopeed
|
||||
vendor: GopeedLab
|
||||
maintainer: GopeedLab <[email protected]>
|
||||
build_arch: $RPM_ARCH
|
||||
EOF
|
||||
|
||||
cat << EOF > distribute_options.yaml
|
||||
output: dist/
|
||||
releases:
|
||||
@@ -366,8 +306,6 @@ jobs:
|
||||
build_args:
|
||||
dart-define:
|
||||
UPDATE_CHANNEL: linuxAppImage
|
||||
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
|
||||
- name: deb
|
||||
package:
|
||||
platform: linux
|
||||
@@ -375,22 +313,12 @@ jobs:
|
||||
build_args:
|
||||
dart-define:
|
||||
UPDATE_CHANNEL: linuxDeb
|
||||
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
|
||||
- name: rpm
|
||||
package:
|
||||
platform: linux
|
||||
target: rpm
|
||||
build_args:
|
||||
dart-define:
|
||||
UPDATE_CHANNEL: linuxRpm
|
||||
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
|
||||
EOF
|
||||
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||
flutter_distributor release --name linux
|
||||
cd dist/*
|
||||
|
||||
# Get current running architecture, x86_64 -> amd64 and aarch64 -> arm64
|
||||
ARCH="amd64"
|
||||
if [[ "${{ matrix.os }}" == *-arm ]]; then
|
||||
ARCH="arm64"
|
||||
@@ -398,7 +326,6 @@ jobs:
|
||||
|
||||
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
|
||||
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
|
||||
mv gopeed-*-linux.rpm Gopeed-v${VERSION}-linux-${ARCH}.rpm
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
@@ -480,7 +407,6 @@ jobs:
|
||||
- cmake
|
||||
- curl
|
||||
- libgtk-3-dev
|
||||
- libkeybinder-3.0-dev
|
||||
- ninja-build
|
||||
- unzip
|
||||
- xz-utils
|
||||
@@ -496,14 +422,13 @@ jobs:
|
||||
plugin: nil
|
||||
stage-packages:
|
||||
- libgtk-3-dev
|
||||
- libkeybinder-3.0-dev
|
||||
- libappindicator3-dev
|
||||
override-build: |
|
||||
# work around pub get stack overflow # https://github.com/dart-lang/sdk/issues/51068#issuecomment-1396588253
|
||||
set +e
|
||||
dart pub get
|
||||
set -eux
|
||||
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap" --dart-define="GA4_MEASUREMENT_ID=$GA4_MEASUREMENT_ID" --dart-define="GA4_API_SECRET=$GA4_API_SECRET"
|
||||
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap"
|
||||
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
||||
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
||||
gpu-2404:
|
||||
@@ -568,8 +493,8 @@ jobs:
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
|
||||
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk"
|
||||
mkdir dist
|
||||
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
|
||||
cp build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/Gopeed-v$VERSION-android-armeabi-v7a.apk
|
||||
@@ -583,7 +508,7 @@ jobs:
|
||||
overwrite: true
|
||||
build-ios:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -602,7 +527,7 @@ jobs:
|
||||
gomobile init
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa"
|
||||
mkdir Payload
|
||||
cp -r build/ios/iphoneos/Runner.app Payload
|
||||
zip -r -y Payload.zip Payload/Runner.app
|
||||
@@ -631,8 +556,7 @@ jobs:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
dart ../../.github/workflows/scripts/flutter_local_font.dart
|
||||
flutter build web --no-web-resources-cdn
|
||||
cd ../../
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
mkdir -p dist/zip
|
||||
@@ -748,8 +672,7 @@ jobs:
|
||||
- name: Build flutter web
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
dart ../../.github/workflows/scripts/flutter_local_font.dart
|
||||
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker"
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
|
||||
@@ -1,412 +0,0 @@
|
||||
// Localize external Google Fonts (fonts.gstatic.com) references in Flutter Web build output.
|
||||
//
|
||||
// Usage (post-build):
|
||||
// # Run AFTER: flutter build web --no-web-resources-cdn
|
||||
// dart ../../.github/workflows/scripts/flutter_local_font.dart
|
||||
//
|
||||
// What it does:
|
||||
// In main.dart.js:
|
||||
// - Find any referenced resources under: https://fonts.gstatic.com/s/
|
||||
// (including the common pattern where Flutter concatenates the base URL with a relative path)
|
||||
// - Download them into: build/web/assets/gstatic/<path>
|
||||
// - Rewrite "https://fonts.gstatic.com/s/" -> "assets/gstatic/" so runtime loads locally
|
||||
// - Only download font subsets needed by supported locales (parsed from message.dart)
|
||||
//
|
||||
// Notes:
|
||||
// - This script DOES NOT call `flutter build web`. It only patches the output.
|
||||
// - This is a best-effort post-process. Always verify in browser devtools.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
/// Get font families required for a specific locale based on language/script.
|
||||
/// Uses language code prefix to automatically detect the script system.
|
||||
/// Returns empty set if base fonts (Latin/Cyrillic/Greek) are sufficient.
|
||||
Set<String> _getFontFamiliesForLocale(String locale) {
|
||||
final lang = locale.toLowerCase().split('_').first;
|
||||
|
||||
// CJK languages - need specific large font files
|
||||
if (lang == 'zh') {
|
||||
// Simplified vs Traditional Chinese
|
||||
if (locale.contains('cn') || locale.contains('sg')) {
|
||||
return {'notosanssc'}; // Simplified Chinese
|
||||
}
|
||||
return {'notosanstc', 'notosanshk'}; // Traditional Chinese (TW, HK, MO)
|
||||
}
|
||||
if (lang == 'ja') return {'notosansjp'}; // Japanese
|
||||
if (lang == 'ko') return {'notosanskr'}; // Korean
|
||||
|
||||
// Arabic script languages
|
||||
if (lang == 'ar') return {'notosansarabic'}; // Arabic
|
||||
if (lang == 'fa') return {'notosansarabic'}; // Persian/Farsi
|
||||
if (lang == 'ur') return {'notosansarabic'}; // Urdu
|
||||
if (lang == 'ps') return {'notosansarabic'}; // Pashto
|
||||
if (lang == 'ku') return {'notosansarabic'}; // Kurdish (Arabic script)
|
||||
|
||||
// Hebrew script
|
||||
if (lang == 'he' || lang == 'yi') return {'notosanshebrew'};
|
||||
|
||||
// South Asian scripts
|
||||
if (lang == 'hi' || lang == 'mr' || lang == 'ne' || lang == 'sa') {
|
||||
return {'notosansdevanagari'}; // Hindi, Marathi, Nepali, Sanskrit
|
||||
}
|
||||
if (lang == 'bn' || lang == 'as')
|
||||
return {'notosansbengali'}; // Bengali, Assamese
|
||||
if (lang == 'ta')
|
||||
return {'notosanstamil', 'notosanstamilsupplement'}; // Tamil
|
||||
if (lang == 'te') return {'notosanstelugu'}; // Telugu
|
||||
if (lang == 'kn') return {'notosanskannada'}; // Kannada
|
||||
if (lang == 'ml') return {'notosansmalayalam'}; // Malayalam
|
||||
if (lang == 'gu') return {'notosansgujarati'}; // Gujarati
|
||||
if (lang == 'pa') return {'notosansgurmukhi'}; // Punjabi (Gurmukhi)
|
||||
if (lang == 'or') return {'notosansoriya'}; // Odia/Oriya
|
||||
if (lang == 'si') return {'notosanssinhala'}; // Sinhala
|
||||
|
||||
// Southeast Asian scripts
|
||||
if (lang == 'th') return {'notosansthai'}; // Thai
|
||||
if (lang == 'lo') return {'notosanslao'}; // Lao
|
||||
if (lang == 'my') return {'notosansmyanmar'}; // Myanmar/Burmese
|
||||
if (lang == 'km') return {'notosanskhmer'}; // Khmer/Cambodian
|
||||
if (lang == 'jv') return {'notosansjavanese'}; // Javanese
|
||||
|
||||
// Other scripts
|
||||
if (lang == 'ka') return {'notosansgeorgian'}; // Georgian
|
||||
if (lang == 'hy') return {'notosansarmenian'}; // Armenian
|
||||
if (lang == 'am' || lang == 'ti')
|
||||
return {'notosansethiopic'}; // Amharic, Tigrinya
|
||||
if (lang == 'mn') return {'notosansmongolian'}; // Mongolian
|
||||
|
||||
// Latin/Cyrillic/Greek based languages - base notosans is sufficient
|
||||
// Includes: English, German, French, Spanish, Italian, Portuguese,
|
||||
// Russian, Ukrainian, Polish, Turkish, Vietnamese, Greek, etc.
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Base font families that are always included regardless of locale.
|
||||
/// These provide core functionality and are relatively small.
|
||||
const _baseFontFamilies = <String>{
|
||||
'notosans', // Base Latin/Cyrillic/Greek/Vietnamese
|
||||
'roboto', // Material Design default font
|
||||
'notosanssymbols', // Common symbols
|
||||
'notosanssymbols2', // Additional symbols
|
||||
'notosansmath', // Math symbols
|
||||
'notomusic', // Music notation symbols
|
||||
// Note: notocoloremoji (~24MB) and notoemoji (~860KB) are excluded
|
||||
// to reduce bundle size. Add them back if emoji support is needed.
|
||||
};
|
||||
|
||||
/// Parse supported locales from message.dart file.
|
||||
/// Reads the import statements and extracts locale codes like 'zh_cn', 'en_us', etc.
|
||||
Set<String> _parseSupportedLocales(File messageFile) {
|
||||
final locales = <String>{};
|
||||
|
||||
if (!messageFile.existsSync()) {
|
||||
_fail('Warning: message.dart not found, only base fonts will be used');
|
||||
}
|
||||
|
||||
final content = messageFile.readAsStringSync();
|
||||
|
||||
// Match import statements like: import 'langs/zh_cn.dart';
|
||||
final importRegex = RegExp(r"import\s+'langs/(\w+)\.dart'");
|
||||
for (final match in importRegex.allMatches(content)) {
|
||||
final locale = match.group(1);
|
||||
if (locale != null) {
|
||||
locales.add(locale);
|
||||
}
|
||||
}
|
||||
|
||||
if (locales.isEmpty) {
|
||||
_fail(
|
||||
'Warning: No locales found in message.dart, only base fonts will be used');
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
/// Get required font families for the given locales.
|
||||
/// Returns a set of font family names that should be included.
|
||||
/// Automatically detects required fonts based on language code prefix.
|
||||
Set<String> _getRequiredFontFamilies(Set<String> locales) {
|
||||
final families = <String>{..._baseFontFamilies};
|
||||
for (final locale in locales) {
|
||||
final localeFamilies = _getFontFamiliesForLocale(locale);
|
||||
families.addAll(localeFamilies);
|
||||
}
|
||||
return families;
|
||||
}
|
||||
|
||||
/// Check if a font file path should be included based on required font families.
|
||||
/// Font paths have the format: "fontfamily/version/filename.ext"
|
||||
/// Example: "notosanssc/v36/xxx.ttf" -> font family is "notosanssc"
|
||||
bool _fontMatchesRequiredFamilies(
|
||||
String fontPath, Set<String> requiredFamilies) {
|
||||
// Extract font family from path (first segment before '/')
|
||||
final slashIndex = fontPath.indexOf('/');
|
||||
if (slashIndex == -1) {
|
||||
// No slash found, include by default (unusual path format)
|
||||
return true;
|
||||
}
|
||||
|
||||
final fontFamily = fontPath.substring(0, slashIndex).toLowerCase();
|
||||
return requiredFamilies.contains(fontFamily);
|
||||
}
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
try {
|
||||
// This script is intentionally argument-free for CI convenience.
|
||||
// It assumes it is executed from the Flutter project directory (ui/flutter),
|
||||
// but will also try to locate pubspec.yaml by walking up.
|
||||
final flutterDir = _findFlutterProjectRoot(Directory.current);
|
||||
|
||||
// Parse supported locales from message.dart
|
||||
final messageFile =
|
||||
File(_join(flutterDir.path, 'lib', 'i18n', 'message.dart'));
|
||||
final supportedLocales = _parseSupportedLocales(messageFile);
|
||||
stdout.writeln('Supported locales: ${supportedLocales.join(', ')}');
|
||||
|
||||
// Get required font families based on supported locales
|
||||
final requiredFamilies = _getRequiredFontFamilies(supportedLocales);
|
||||
stdout.writeln('Required font families: ${requiredFamilies.join(', ')}');
|
||||
|
||||
final webDir = Directory(_join(flutterDir.path, 'build', 'web'));
|
||||
if (!webDir.existsSync()) {
|
||||
_fail(
|
||||
'Web build directory not found: ${webDir.path}\n'
|
||||
'Did you run: flutter build web --no-web-resources-cdn ?',
|
||||
);
|
||||
}
|
||||
|
||||
final mainJs = File(_join(webDir.path, 'main.dart.js'));
|
||||
if (!mainJs.existsSync()) {
|
||||
_fail('main.dart.js not found at: ${mainJs.path}');
|
||||
}
|
||||
|
||||
final gstaticRoot = Directory(_join(webDir.path, 'assets', 'gstatic'));
|
||||
if (!gstaticRoot.existsSync()) gstaticRoot.createSync(recursive: true);
|
||||
|
||||
const gstaticSPrefix = 'https://fonts.gstatic.com/s/';
|
||||
final original = mainJs.readAsStringSync();
|
||||
|
||||
// Relative font asset paths which Flutter's loader typically concatenates with:
|
||||
// https://fonts.gstatic.com/s/
|
||||
// Keep this as a best-effort heuristic to cover the common concatenation pattern.
|
||||
// Some gstatic assets include extra dot segments like: notocoloremoji/v32/Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabsE4tq3luCC7p-aXxcn.0.woff2
|
||||
// Allow dots in the path, while still restricting to font-like extensions.
|
||||
final relAssetRegex = RegExp(
|
||||
r"""["']([a-zA-Z0-9/_\.-]+\.(?:woff2|woff|ttf|otf|eot|svg))["']""",
|
||||
caseSensitive: false,
|
||||
);
|
||||
final relAssetsUnderS = <String>{};
|
||||
for (final m in relAssetRegex.allMatches(original)) {
|
||||
final p = m.group(1);
|
||||
if (p == null || p.isEmpty) continue;
|
||||
if (p.contains('://') || p.startsWith('data:')) continue;
|
||||
if (p.startsWith('/') ||
|
||||
p.startsWith('assets/') ||
|
||||
p.startsWith('packages/')) continue;
|
||||
if (p.contains('..')) continue;
|
||||
relAssetsUnderS.add(p);
|
||||
}
|
||||
|
||||
if (relAssetsUnderS.isEmpty) {
|
||||
_fail('No fonts.gstatic.com assets found in main.dart.js.');
|
||||
}
|
||||
|
||||
// Build a download plan: dest relative path (under assets/gstatic/) -> URL.
|
||||
// Filter fonts based on required font families to reduce package size.
|
||||
final downloads = <String, Uri>{};
|
||||
final skippedFonts = <String, Set<String>>{};
|
||||
|
||||
for (final rel in relAssetsUnderS) {
|
||||
if (_fontMatchesRequiredFamilies(rel, requiredFamilies)) {
|
||||
downloads[rel] = Uri.parse('$gstaticSPrefix$rel');
|
||||
} else {
|
||||
// Track skipped fonts and their paths for fallback generation
|
||||
final slashIndex = rel.indexOf('/');
|
||||
final family = slashIndex > 0 ? rel.substring(0, slashIndex) : rel;
|
||||
skippedFonts.putIfAbsent(family, () => <String>{}).add(rel);
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedFonts.isNotEmpty) {
|
||||
stdout.writeln(
|
||||
'Skipped ${skippedFonts.length} font families not required by supported locales:',
|
||||
);
|
||||
stdout.writeln(' ${skippedFonts.keys.join(', ')}');
|
||||
}
|
||||
|
||||
if (downloads.isNotEmpty) {
|
||||
stdout.writeln(
|
||||
'Found ${downloads.length} fonts.gstatic.com assets to download...',
|
||||
);
|
||||
|
||||
// Reuse a single HttpClient to avoid creating hundreds of short-lived
|
||||
// connections (can be flaky on some environments).
|
||||
final httpClient = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 30)
|
||||
..idleTimeout = const Duration(seconds: 30)
|
||||
..maxConnectionsPerHost = 6;
|
||||
try {
|
||||
for (final entry in downloads.entries) {
|
||||
final relPath = entry.key;
|
||||
final url = entry.value;
|
||||
final destPath = relPath.replaceAll('/', Platform.pathSeparator);
|
||||
final dest = File(_join(gstaticRoot.path, destPath));
|
||||
await _downloadIfMissing(httpClient, url, dest);
|
||||
}
|
||||
} finally {
|
||||
httpClient.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate empty fallback font files for skipped fonts to prevent infinite loading
|
||||
// Empty files will cause browsers to fail fast instead of retrying indefinitely
|
||||
if (skippedFonts.isNotEmpty) {
|
||||
var fallbackCount = 0;
|
||||
for (final entry in skippedFonts.entries) {
|
||||
for (final relPath in entry.value) {
|
||||
final destPath = relPath.replaceAll('/', Platform.pathSeparator);
|
||||
final dest = File(_join(gstaticRoot.path, destPath));
|
||||
if (!dest.existsSync()) {
|
||||
dest.parent.createSync(recursive: true);
|
||||
// Create an empty file - browsers will recognize it as invalid and skip
|
||||
dest.writeAsBytesSync([]);
|
||||
fallbackCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fallbackCount > 0) {
|
||||
stdout.writeln(
|
||||
'Generated $fallbackCount empty fallback font files to prevent infinite loading',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite remote prefix so requests become:
|
||||
// <origin>/assets/gstatic/<...>.woff2
|
||||
final replacedGstatic = original.replaceAll(
|
||||
gstaticSPrefix,
|
||||
'assets/gstatic/',
|
||||
);
|
||||
|
||||
if (replacedGstatic == original && downloads.isEmpty) {
|
||||
stdout.writeln(
|
||||
'No changes applied (no fonts.gstatic.com references found).',
|
||||
);
|
||||
exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
mainJs.writeAsStringSync(replacedGstatic);
|
||||
|
||||
stdout.writeln('Patched: ${mainJs.path}');
|
||||
if (downloads.isNotEmpty) {
|
||||
stdout.writeln(
|
||||
' - downloaded ${downloads.length} files into: ${gstaticRoot.path}',
|
||||
);
|
||||
}
|
||||
stdout.writeln(' - fonts.gstatic.com rewritten -> assets/gstatic/');
|
||||
} catch (e, st) {
|
||||
stderr.writeln('ERROR: $e');
|
||||
stderr.writeln(st);
|
||||
exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _downloadIfMissing(HttpClient client, Uri url, File dest) async {
|
||||
if (dest.existsSync() && dest.lengthSync() > 0) return;
|
||||
|
||||
dest.parent.createSync(recursive: true);
|
||||
|
||||
// Network can be flaky in CI. Retry a few times on transient errors.
|
||||
const maxAttempts = 4;
|
||||
|
||||
for (var attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
final tmp = File('${dest.path}.tmp');
|
||||
try {
|
||||
try {
|
||||
final req = await client.getUrl(url);
|
||||
|
||||
final resp = await req.close().timeout(const Duration(seconds: 60));
|
||||
if (resp.statusCode != 200) {
|
||||
// 404 is not transient in practice - fail fast with a clear message.
|
||||
if (resp.statusCode == 404) {
|
||||
_fail('Missing gstatic asset (404): $url');
|
||||
}
|
||||
throw HttpException('HTTP ${resp.statusCode}', uri: url);
|
||||
}
|
||||
|
||||
// Stream to temp file first, then rename to avoid leaving partial files.
|
||||
if (tmp.existsSync()) tmp.deleteSync();
|
||||
final sink = tmp.openWrite();
|
||||
await resp.pipe(sink).timeout(const Duration(seconds: 120));
|
||||
|
||||
if (!tmp.existsSync() || tmp.lengthSync() == 0) {
|
||||
throw const FormatException('Downloaded asset is empty');
|
||||
}
|
||||
if (dest.existsSync()) dest.deleteSync();
|
||||
tmp.renameSync(dest.path);
|
||||
return;
|
||||
} finally {
|
||||
// no-op: HttpClient lifecycle managed by the caller
|
||||
}
|
||||
} on TimeoutException {
|
||||
if (tmp.existsSync()) tmp.deleteSync();
|
||||
if (attempt == maxAttempts) {
|
||||
_fail('Timeout downloading asset: $url');
|
||||
}
|
||||
} on SocketException catch (e) {
|
||||
if (tmp.existsSync()) tmp.deleteSync();
|
||||
if (attempt == maxAttempts) {
|
||||
_fail('Socket error downloading asset: $url ($e)');
|
||||
}
|
||||
} on HttpException catch (e) {
|
||||
if (tmp.existsSync()) tmp.deleteSync();
|
||||
if (attempt == maxAttempts) {
|
||||
_fail('HTTP error downloading asset: $url ($e)');
|
||||
}
|
||||
} on FileSystemException catch (e) {
|
||||
if (tmp.existsSync()) tmp.deleteSync();
|
||||
_fail('File write error downloading asset: $url ($e)');
|
||||
} catch (e) {
|
||||
if (tmp.existsSync()) tmp.deleteSync();
|
||||
if (attempt == maxAttempts) {
|
||||
_fail('Unexpected error downloading asset: $url ($e)');
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff before retrying.
|
||||
final backoffSeconds = 1 << (attempt - 1);
|
||||
stdout.writeln(
|
||||
'Retrying (${attempt + 1}/$maxAttempts) for: $url (wait ${backoffSeconds}s)',
|
||||
);
|
||||
await Future.delayed(Duration(seconds: backoffSeconds));
|
||||
}
|
||||
}
|
||||
|
||||
Directory _findFlutterProjectRoot(Directory start) {
|
||||
var dir = start;
|
||||
for (var i = 0; i < 10; i++) {
|
||||
final pubspec = File(_join(dir.path, 'pubspec.yaml'));
|
||||
if (pubspec.existsSync()) return dir;
|
||||
final parent = dir.parent;
|
||||
if (parent.path == dir.path) break;
|
||||
dir = parent;
|
||||
}
|
||||
// Fallback: use current directory, error messages will explain what is missing.
|
||||
return start;
|
||||
}
|
||||
|
||||
Never _fail(String msg) {
|
||||
throw FormatException(msg);
|
||||
}
|
||||
|
||||
String _join(String a, [String? b, String? c, String? d]) {
|
||||
final parts = <String>[
|
||||
a,
|
||||
if (b != null) b,
|
||||
if (c != null) c,
|
||||
if (d != null) d,
|
||||
];
|
||||
return parts.join(Platform.pathSeparator);
|
||||
}
|
||||
+13
-56
@@ -25,11 +25,10 @@ on:
|
||||
- ".github/workflows/test.yml"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.24"
|
||||
FLUTTER_VERSION: "3.41.2"
|
||||
FLUTTER_VERSION: "3.24.5"
|
||||
|
||||
jobs:
|
||||
# lint:
|
||||
@@ -54,68 +53,27 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Run Unit tests.
|
||||
run: |
|
||||
# Get packages for testing and coverage (including cmd/web for flags_test.go)
|
||||
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
|
||||
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
|
||||
# Filter out main.go from coverage report to avoid lowering coverage percentage
|
||||
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
|
||||
mv coverage_filtered.txt coverage.txt
|
||||
run: go test -v -coverpkg=$(go list ./... | grep -v /cmd/ | grep -v /bind/ | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /cmd/ | grep -v /bind/)
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
build-desktop:
|
||||
name: Build desktop check
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-2022
|
||||
- os: windows-11-arm
|
||||
llvm_ver: "20251202" # Only ARM64
|
||||
flutter_channel: "main"
|
||||
flutter_version: "7e1c8868"
|
||||
- os: macos-latest
|
||||
- os: ubuntu-22.04
|
||||
- os: ubuntu-22.04-arm
|
||||
flutter_channel: "main"
|
||||
name: Build desktop check (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [test]
|
||||
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Enable long paths for flutter main branch checks
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- name: Install llvm-mingw-ucrt-aarch64 (ARM64 only)
|
||||
if: matrix.os == 'windows-11-arm'
|
||||
run: |
|
||||
$ver = "${{ matrix.llvm_ver }}"
|
||||
$url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$ver/llvm-mingw-$ver-ucrt-aarch64.zip"
|
||||
$zip = "$env:RUNNER_TEMP\\llvm.zip"
|
||||
$extract = "$env:RUNNER_TEMP\\extract"
|
||||
$target = "C:\\clangarm64"
|
||||
|
||||
curl -L $url -o $zip
|
||||
rm -r -fo $extract,$target -ea Ignore
|
||||
mkdir $extract | Out-Null
|
||||
|
||||
tar -xf $zip -C $extract
|
||||
mv (Get-ChildItem $extract)[0].FullName $target
|
||||
|
||||
$b = "$target\\bin"
|
||||
"CC=$b\\clang.exe" >> $env:GITHUB_ENV
|
||||
"CXX=$b\\clang++.exe" >> $env:GITHUB_ENV
|
||||
"CLANGARM64_BIN=$b" >> $env:GITHUB_ENV
|
||||
"CGO_ENABLED=1" >> $env:GITHUB_ENV
|
||||
"CLANGARM64_ROOT=$target" >> $env:GITHUB_ENV
|
||||
$b >> $env:GITHUB_PATH
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: ${{ matrix.flutter_channel || 'stable' }}
|
||||
flutter-version: ${{ matrix.flutter_version || env.FLUTTER_VERSION }}
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: master
|
||||
- if: runner.os == 'Windows'
|
||||
run: |
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||
@@ -129,7 +87,7 @@ jobs:
|
||||
- if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev libkeybinder-3.0-dev
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||
cd ui/flutter
|
||||
flutter build linux
|
||||
@@ -138,8 +96,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-14, ubuntu-latest]
|
||||
needs: [test]
|
||||
os: [ macos-latest, ubuntu-latest ]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -171,7 +129,7 @@ jobs:
|
||||
build-web:
|
||||
name: Build web check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -184,7 +142,6 @@ jobs:
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn
|
||||
dart ../../.github/workflows/scripts/flutter_local_font.dart
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
@@ -192,7 +149,7 @@ jobs:
|
||||
build-docker:
|
||||
name: Build docker check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.11-alpine3.23 AS go
|
||||
FROM golang:1.23.3 AS go
|
||||
WORKDIR /app
|
||||
COPY ./go.mod ./go.sum ./
|
||||
RUN go mod download
|
||||
@@ -8,7 +8,7 @@ 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.23
|
||||
FROM alpine:3.18
|
||||
LABEL maintainer="monkeyWie"
|
||||
WORKDIR /app
|
||||
COPY --from=go /app/dist/gopeed ./
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# [](https://gopeed.com)
|
||||
# [](https://gopeed.com)
|
||||
|
||||
[](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://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,33 +15,25 @@
|
||||
|
||||
## 🚀 Introduction
|
||||
|
||||
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet, ED2K) 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://gopeed.com/docs/dev-api) or installation and development of [extensions](https://gopeed.com/docs/dev-extension).
|
||||
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://gopeed.com/docs)
|
||||
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
|
||||
|
||||
## ⬇️ Download
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="4">🪟 Windows</td>
|
||||
<td rowspan="2"><code>EXE</code></td>
|
||||
<td rowspan="2">🪟 Windows</td>
|
||||
<td><code>EXE</code></td>
|
||||
<td>amd64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>arm64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-arm64.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2"><code>Portable</code></td>
|
||||
<td><code>Portable</code></td>
|
||||
<td>amd64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>arm64</td>
|
||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-arm64-portable.zip">📥</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">🍎 MacOS</td>
|
||||
<td rowspan="3"><code>DMG</code></td>
|
||||
@@ -165,7 +156,7 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
More about installation, please refer to [Installation](https://gopeed.com/docs/install)
|
||||
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||
|
||||
### 🛠️ Command tool
|
||||
|
||||
@@ -179,15 +170,9 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
|
||||
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
||||
|
||||
## 📱 WeChat Official Account
|
||||
|
||||
Follow our WeChat Official Account to get the latest updates and news.
|
||||
|
||||
<img src="_docs/img/weixin.png" width="200" />
|
||||
|
||||
## 💝 Donate
|
||||
|
||||
If you like this project, please consider [donating](https://gopeed.com/docs/donate) to support the development of this project, thank you!
|
||||
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
|
||||
|
||||
## 🖼️ Showcase
|
||||
|
||||
@@ -201,8 +186,8 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
||||
|
||||
### 🌍 Environment
|
||||
|
||||
1. Golang 1.24+
|
||||
2. Flutter 3.38+
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 📋 Clone
|
||||
|
||||
|
||||
+8
-15
@@ -1,11 +1,10 @@
|
||||
# [](https://gopeed.com)
|
||||
# [](https://gopeed.com)
|
||||
|
||||
[](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://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,9 +15,9 @@
|
||||
|
||||
## 🚀 はじめに
|
||||
|
||||
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet、ED2K) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://gopeed.com/docs/dev-api)との連動や[拡張機能](https://gopeed.com/docs/dev-extension)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
|
||||
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
|
||||
|
||||
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://gopeed.com/docs)
|
||||
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
|
||||
|
||||
## ⬇️ インストール
|
||||
|
||||
@@ -156,19 +155,13 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
インストールについての詳細は、[インストール](https://gopeed.com/docs/install)を参照してください。
|
||||
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||
|
||||
### 🛠️ コマンドツール
|
||||
|
||||
## 📱 WeChat 公式アカウント
|
||||
|
||||
公式アカウントをフォローして、最新のアップデートやニュースを入手してください。
|
||||
|
||||
<img src="_docs/img/weixin.png" width="200" />
|
||||
|
||||
## 💝 寄付
|
||||
|
||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://gopeed.com/docs/donate)をご検討ください!
|
||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||
|
||||
## 🖼️ ショーケース
|
||||
|
||||
@@ -182,8 +175,8 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
||||
|
||||
### 🌍 環境
|
||||
|
||||
1. Go 言語 1.24+
|
||||
2. Flutter 3.38+
|
||||
1. Go 言語 1.23+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 📋 クローン
|
||||
|
||||
|
||||
+8
-15
@@ -1,11 +1,10 @@
|
||||
# [](https://gopeed.com)
|
||||
# [](https://gopeed.com)
|
||||
|
||||
[](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://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,9 +15,9 @@
|
||||
|
||||
## 🚀 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, ED2K) 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://gopeed.com/docs/dev-api) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://gopeed.com/docs/dev-extension).
|
||||
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://gopeed.com/docs)
|
||||
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ề
|
||||
|
||||
@@ -156,7 +155,7 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://gopeed.com/docs/install)
|
||||
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
|
||||
|
||||
### 🛠️ Công cụ lệnh
|
||||
|
||||
@@ -166,15 +165,9 @@ Sử dụng `go install`:
|
||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
```
|
||||
|
||||
## 📱 WeChat Official Account
|
||||
|
||||
Theo dõi tài khoản chính thức để nhận các cập nhật và tin tức mới nhất.
|
||||
|
||||
<img src="_docs/img/weixin.png" width="200" />
|
||||
|
||||
## 💝 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://gopeed.com/docs/donate) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
||||
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
||||
|
||||
## 🖼️ Trưng bày
|
||||
|
||||
@@ -188,8 +181,8 @@ Dự án này được chia thành hai phần, phần giao diện sử dụng `f
|
||||
|
||||
### 🌍 Environment
|
||||
|
||||
1. Golang 1.24+
|
||||
2. Flutter 3.38+
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 📋 Clone
|
||||
|
||||
|
||||
+8
-15
@@ -1,11 +1,10 @@
|
||||
# [](https://gopeed.com)
|
||||
# [](https://gopeed.com)
|
||||
|
||||
[](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://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://docs.gopeed.com/zh/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,9 +15,9 @@
|
||||
|
||||
## 🚀 介绍
|
||||
|
||||
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet、ED2K)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://gopeed.com/docs/dev-api)或者安装和开发[扩展](https://gopeed.com/docs/dev-extension)来实现更多的功能。
|
||||
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://gopeed.com/docs)
|
||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
|
||||
|
||||
## ⬇️ 下载
|
||||
|
||||
@@ -156,7 +155,7 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
更多关于安装的内容请参考[安装文档](https://gopeed.com/docs/install)
|
||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||
|
||||
### 🛠️ 命令行工具
|
||||
|
||||
@@ -170,15 +169,9 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
|
||||
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
||||
|
||||
## 📱 微信公众号
|
||||
|
||||
关注公众号获取项目最新动态和资讯。
|
||||
|
||||
<img src="_docs/img/weixin.png" width="200" />
|
||||
|
||||
## 💝 赞助
|
||||
|
||||
如果觉得项目对你有帮助,请考虑[赞助](https://gopeed.com/docs/donate)以支持这个项目的发展,非常感谢!
|
||||
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
||||
|
||||
## 🖼️ 界面展示
|
||||
|
||||
@@ -192,8 +185,8 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
|
||||
|
||||
### 🌍 环境要求
|
||||
|
||||
1. Golang 1.24+
|
||||
2. Flutter 3.38+
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 📋 克隆项目
|
||||
|
||||
|
||||
+8
-15
@@ -1,11 +1,10 @@
|
||||
# [](https://gopeed.com)
|
||||
# [](https://gopeed.com)
|
||||
|
||||
[](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://gopeed.com/docs/donate)
|
||||
[](https://raw.githubusercontent.com/GopeedLab/gopeed/main/_docs/img/weixin.png)
|
||||
[](https://docs.gopeed.com/donate.html)
|
||||
[](https://discord.gg/ZUJqJrwCGB)
|
||||
|
||||
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,9 +15,9 @@
|
||||
|
||||
## 🚀 簡介
|
||||
|
||||
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet、ED2K)協定,同時支援所有的平台。
|
||||
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
|
||||
|
||||
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://gopeed.com/docs)
|
||||
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
||||
|
||||
## ⬇️ 下載
|
||||
|
||||
@@ -157,7 +156,7 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
更多關於安裝的內容請參考[安裝文檔](https://gopeed.com/docs/install)
|
||||
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||
|
||||
### 🛠️ 使用 CLI 安裝
|
||||
|
||||
@@ -167,15 +166,9 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
```
|
||||
|
||||
## 📱 微信公眾號
|
||||
|
||||
關注公眾號獲取項目最新動態和資訊。
|
||||
|
||||
<img src="_docs/img/weixin.png" width="200" />
|
||||
|
||||
## 💝 贊助
|
||||
|
||||
如果你認為該項目對你有所幫助,請考慮[贊助](https://gopeed.com/docs/donate)以支持該項目的持續發展,謝謝!
|
||||
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||
|
||||
## 🖼️ 軟體介面
|
||||
|
||||
@@ -189,8 +182,8 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||
|
||||
### 🌍 開發環境
|
||||
|
||||
1. Golang 1.24+
|
||||
2. Flutter 3.38+
|
||||
1. Golang 1.23+
|
||||
2. Flutter 3.24+
|
||||
|
||||
### 📋 克隆項目
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -1,54 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 400" width="1200" height="400">
|
||||
<rect width="1200" height="400" fill="#040605"/>
|
||||
<g fill="#5dc76c">
|
||||
<rect x="-100" y="50" width="150" height="2" opacity="0.6">
|
||||
<animate attributeName="x" from="-200" to="1300" dur="0.8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="-200" y="150" width="200" height="1" opacity="0.4">
|
||||
<animate attributeName="x" from="-300" to="1300" dur="1.2s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="-50" y="250" width="180" height="3" opacity="0.8">
|
||||
<animate attributeName="x" from="-200" to="1300" dur="0.6s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="-150" y="350" width="220" height="2" opacity="0.5">
|
||||
<animate attributeName="x" from="-300" to="1300" dur="1s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="-100" y="100" width="100" height="1" opacity="0.7">
|
||||
<animate attributeName="x" from="-150" to="1300" dur="0.9s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="-250" y="300" width="250" height="2" opacity="0.3">
|
||||
<animate attributeName="x" from="-400" to="1300" dur="1.5s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="logoGlow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="12" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(600, 160)">
|
||||
<!-- Floating animation -->
|
||||
<g>
|
||||
<animateTransform attributeName="transform" type="translate" values="0,-8; 0,8; 0,-8" dur="3s" repeatCount="indefinite" />
|
||||
|
||||
<!-- Pulsing glow behind logo -->
|
||||
<circle cx="0" cy="0" r="45" fill="#5dc76c" filter="url(#logoGlow)" opacity="0.3">
|
||||
<animate attributeName="opacity" values="0.1; 0.6; 0.1" dur="2s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="40; 65; 40" dur="2s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<!-- Breathing scale animation -->
|
||||
<g>
|
||||
<animateTransform attributeName="transform" type="scale" values="0.95; 1.05; 0.95" dur="2s" repeatCount="indefinite" />
|
||||
<g transform="scale(0.12) translate(-512, -512)">
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<text x="600" y="300" font-family="sans-serif" font-size="72" font-style="italic" font-weight="900" text-anchor="middle" fill="#fff" letter-spacing="8">GOPEED</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 309 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
|
||||
+2
-3
@@ -2,14 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const progressWidth = 20
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Dial() (net.Conn, error) {
|
||||
// Get binary path
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return net.Dial("unix", filepath.Join(filepath.Dir(exe), "gopeed_host.sock"))
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
)
|
||||
|
||||
func Dial() (net.Conn, error) {
|
||||
return winio.DialPipe(`\\.\pipe\gopeed_host`, nil)
|
||||
}
|
||||
+26
-111
@@ -1,24 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
)
|
||||
|
||||
const identifier = "gopeed"
|
||||
|
||||
type Message struct {
|
||||
Method string `json:"method"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
@@ -28,117 +26,33 @@ type Response struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func check() (data bool, err error) {
|
||||
conn, err := Dial()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func wakeup(hidden bool) error {
|
||||
running, _ := check()
|
||||
if running {
|
||||
return nil
|
||||
}
|
||||
|
||||
uri := "gopeed:"
|
||||
if hidden {
|
||||
uri = uri + "?hidden=true"
|
||||
}
|
||||
if err := browser.OpenURL(uri); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
if ok, _ := check(); ok {
|
||||
return nil
|
||||
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
|
||||
"ping": func(params json.RawMessage) (data any, err error) {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
return fmt.Errorf("start gopeed failed")
|
||||
}
|
||||
|
||||
// postToFlutter sends a POST request to Flutter RPC server
|
||||
func postToFlutter(path string, body []byte, headers map[string]string, timeout time.Duration) (*http.Response, error) {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return Dial()
|
||||
},
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
req, err := http.NewRequest("POST", "http://127.0.0.1"+path, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
return client.Do(req)
|
||||
}
|
||||
for _, p := range processes {
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var apiMap = map[string]func(message *Message) (data any, err error){
|
||||
"ping": func(message *Message) (data any, err error) {
|
||||
return check()
|
||||
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
},
|
||||
"wakeup": func(message *Message) (data any, err error) {
|
||||
silent := false
|
||||
if v, ok := message.Meta["silent"]; ok {
|
||||
silent, _ = v.(bool)
|
||||
"create": func(params json.RawMessage) (data any, err error) {
|
||||
var strParams string
|
||||
if err = json.Unmarshal(params, &strParams); err != nil {
|
||||
return
|
||||
}
|
||||
err = wakeup(silent)
|
||||
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
|
||||
return
|
||||
},
|
||||
"create": func(message *Message) (data any, err error) {
|
||||
buf, err := message.Params.MarshalJSON()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
silent := false
|
||||
if v, ok := message.Meta["silent"]; ok {
|
||||
silent, _ = v.(bool)
|
||||
}
|
||||
|
||||
if err := wakeup(silent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
if message.Meta != nil {
|
||||
metaJson, _ := json.Marshal(message.Meta)
|
||||
headers["X-Gopeed-Host-Meta"] = string(metaJson)
|
||||
}
|
||||
_, err = postToFlutter("/create", buf, headers, 10*time.Second)
|
||||
return
|
||||
},
|
||||
"forward": func(message *Message) (data any, err error) {
|
||||
buf, err := message.Params.MarshalJSON()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := postToFlutter("/forward", buf, nil, 60*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var respData map[string]json.RawMessage
|
||||
if err := json.Unmarshal(respBody, &respData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return respData, nil
|
||||
},
|
||||
}
|
||||
|
||||
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/host
|
||||
@@ -149,6 +63,7 @@ func main() {
|
||||
var length uint32
|
||||
if err := binary.Read(os.Stdin, binary.NativeEndian, &length); err != nil {
|
||||
if err == io.EOF {
|
||||
// Connection closed by Chrome
|
||||
return
|
||||
}
|
||||
sendError("Failed to read message length: " + err.Error())
|
||||
@@ -173,7 +88,7 @@ func main() {
|
||||
var data any
|
||||
var err error
|
||||
if handler, ok := apiMap[message.Method]; ok {
|
||||
data, err = handler(&message)
|
||||
data, err = handler(message.Params)
|
||||
} else {
|
||||
err = errors.New("Unknown method: " + message.Method)
|
||||
}
|
||||
|
||||
+6
-5
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -18,7 +19,6 @@ func main() {
|
||||
pid := flag.Int("pid", 0, "PID of the process to update")
|
||||
updateChannel := flag.String("channel", "", "Update channel")
|
||||
packagePath := flag.String("asset", "", "Path to the package asset")
|
||||
exeDir := flag.String("exeDir", "", "Directory of the entry executable")
|
||||
logPath := flag.String("log", "", "Log file path")
|
||||
flag.Parse()
|
||||
|
||||
@@ -43,7 +43,7 @@ func main() {
|
||||
restart bool
|
||||
err error
|
||||
)
|
||||
if restart, err = update(*pid, *updateChannel, *packagePath, *exeDir); err != nil {
|
||||
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
|
||||
log.Printf("Update failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, err error) {
|
||||
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
|
||||
killSignalChan := make(chan any, 1)
|
||||
|
||||
go func() {
|
||||
@@ -76,8 +76,9 @@ func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, e
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("Updating process updateChannel=%s packagePath=%s exeDir=%s\n", updateChannel, packagePath, exeDir)
|
||||
if restart, err = install(killSignalChan, updateChannel, packagePath, exeDir); err != nil {
|
||||
appDir := filepath.Dir(os.Args[0])
|
||||
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
|
||||
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
|
||||
return false, errors.Wrap(err, "failed to install package")
|
||||
}
|
||||
|
||||
|
||||
@@ -25,20 +25,16 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
|
||||
mountPoint := ""
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
if strings.Contains(line, "/Volumes/") {
|
||||
// Find the /Volumes/ path in the line
|
||||
// hdiutil output format: /dev/disk4s1 Apple_HFS /Volumes/Gopeed
|
||||
// or with sequence number: /dev/disk4s1 Apple_HFS /Volumes/Gopeed 1
|
||||
idx := strings.Index(line, "/Volumes/")
|
||||
if idx != -1 {
|
||||
// Extract everything from /Volumes/ onwards and trim whitespace
|
||||
mountPoint = strings.TrimSpace(line[idx:])
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 2 {
|
||||
mountPoint = fields[len(fields)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mountPoint == "" {
|
||||
return fmt.Errorf("failed to get mount point from hdiutil output: %s", string(output))
|
||||
return fmt.Errorf("failed to get mount point")
|
||||
}
|
||||
|
||||
// Detach the mounted DMG
|
||||
@@ -49,7 +45,7 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
|
||||
return err
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return fmt.Errorf("no .app found in dmg, mountPoint: %s", mountPoint)
|
||||
return fmt.Errorf("no .app found in dmg")
|
||||
}
|
||||
|
||||
killSignalChan <- nil
|
||||
|
||||
+51
-166
@@ -3,23 +3,19 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
type args struct {
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||
|
||||
@@ -27,173 +23,62 @@ type args struct {
|
||||
}
|
||||
|
||||
func parse() *args {
|
||||
cfg := &args{}
|
||||
|
||||
cliConfig := loadCliArgs()
|
||||
loadConfigFile(cfg, *cliConfig.configPath)
|
||||
loadEnvVars(cfg)
|
||||
// override with non-default command line arguments
|
||||
overrideWithCliArgs(cfg, cliConfig)
|
||||
// set default values
|
||||
setDefaults(cfg, cliConfig)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// loadCliArgs parses command line arguments and returns initial config
|
||||
func loadCliArgs() *args {
|
||||
cfg := &args{}
|
||||
cfg.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
cfg.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cfg.Username = flag.String("u", "gopeed", "Web Authentication Username")
|
||||
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||
cfg.StorageDir = flag.String("d", "", "Storage directory")
|
||||
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
|
||||
cfg.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
var cliArgs 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", "Web Authentication Username")
|
||||
cliArgs.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
flag.Parse()
|
||||
|
||||
// Parse white download directories from comma-separated string
|
||||
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
|
||||
dirs := strings.Split(*whiteDownloadDirs, ",")
|
||||
for i := range dirs {
|
||||
dirs[i] = strings.TrimSpace(dirs[i])
|
||||
}
|
||||
cfg.WhiteDownloadDirs = dirs
|
||||
// args priority: config file > cli args
|
||||
cfgArgs := loadConfig(*cliArgs.configPath)
|
||||
if cfgArgs.Address == nil {
|
||||
cfgArgs.Address = cliArgs.Address
|
||||
}
|
||||
|
||||
return cfg
|
||||
if cfgArgs.Port == nil {
|
||||
cfgArgs.Port = cliArgs.Port
|
||||
}
|
||||
if cfgArgs.Username == nil {
|
||||
cfgArgs.Username = cliArgs.Username
|
||||
}
|
||||
if cfgArgs.Password == nil {
|
||||
cfgArgs.Password = cliArgs.Password
|
||||
}
|
||||
if cfgArgs.ApiToken == nil {
|
||||
cfgArgs.ApiToken = cliArgs.ApiToken
|
||||
}
|
||||
if cfgArgs.StorageDir == nil {
|
||||
cfgArgs.StorageDir = cliArgs.StorageDir
|
||||
}
|
||||
return cfgArgs
|
||||
}
|
||||
|
||||
// overrideWithCliArgs overrides config with non-empty command line arguments
|
||||
func overrideWithCliArgs(cfg *args, cliConfig *args) {
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "A":
|
||||
cfg.Address = cliConfig.Address
|
||||
case "P":
|
||||
cfg.Port = cliConfig.Port
|
||||
case "u":
|
||||
cfg.Username = cliConfig.Username
|
||||
case "p":
|
||||
cfg.Password = cliConfig.Password
|
||||
case "T":
|
||||
cfg.ApiToken = cliConfig.ApiToken
|
||||
case "d":
|
||||
cfg.StorageDir = cliConfig.StorageDir
|
||||
case "w":
|
||||
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
|
||||
case "c":
|
||||
cfg.configPath = cliConfig.configPath
|
||||
}
|
||||
})
|
||||
}
|
||||
func loadConfig(path string) *args {
|
||||
var args args
|
||||
|
||||
// setDefaults sets default values for any unset configuration fields
|
||||
func setDefaults(cfg *args, cliConfig *args) {
|
||||
if cfg.Address == nil {
|
||||
cfg.Address = cliConfig.Address
|
||||
}
|
||||
if cfg.Port == nil {
|
||||
cfg.Port = cliConfig.Port
|
||||
}
|
||||
if cfg.Username == nil {
|
||||
cfg.Username = cliConfig.Username
|
||||
}
|
||||
if cfg.Password == nil {
|
||||
cfg.Password = cliConfig.Password
|
||||
}
|
||||
if cfg.ApiToken == nil {
|
||||
cfg.ApiToken = cliConfig.ApiToken
|
||||
}
|
||||
if cfg.StorageDir == nil {
|
||||
cfg.StorageDir = cliConfig.StorageDir
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfigFile loads configuration from file
|
||||
func loadConfigFile(cfg *args, configPath string) {
|
||||
if !filepath.IsAbs(configPath) {
|
||||
if !filepath.IsAbs(path) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
configPath = filepath.Join(dir, configPath)
|
||||
path = filepath.Join(dir, path)
|
||||
}
|
||||
|
||||
file, err := os.ReadFile(configPath)
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
return &args
|
||||
}
|
||||
return
|
||||
fmt.Println("config file read failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(file, cfg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
|
||||
func loadEnvVars(cfg *args) {
|
||||
v := reflect.ValueOf(cfg).Elem()
|
||||
t := reflect.TypeOf(cfg).Elem()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
fieldType := t.Field(i)
|
||||
|
||||
// Get json tag as environment variable suffix
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
if jsonTag == "" || jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove options like omitempty
|
||||
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
|
||||
jsonTag = jsonTag[:commaIdx]
|
||||
}
|
||||
|
||||
// Convert to uppercase and add GOPEED_ prefix
|
||||
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
|
||||
envValue := os.Getenv(envKey)
|
||||
|
||||
if envValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set value based on field type
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
// Create new pointer instance
|
||||
newVal := reflect.New(field.Type().Elem())
|
||||
field.Set(newVal)
|
||||
}
|
||||
|
||||
switch field.Type().Elem().Kind() {
|
||||
case reflect.String:
|
||||
field.Elem().SetString(envValue)
|
||||
case reflect.Int:
|
||||
if intVal, err := strconv.Atoi(envValue); err == nil {
|
||||
field.Elem().SetInt(int64(intVal))
|
||||
}
|
||||
default:
|
||||
// For complex types like DownloadConfig, try JSON unmarshaling
|
||||
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
|
||||
var config base.DownloaderStoreConfig
|
||||
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
|
||||
field.Set(reflect.ValueOf(&config))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if field.Kind() == reflect.Slice {
|
||||
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
|
||||
if field.Type().Elem().Kind() == reflect.String {
|
||||
dirs := strings.Split(envValue, ",")
|
||||
for i := range dirs {
|
||||
dirs[i] = strings.TrimSpace(dirs[i])
|
||||
}
|
||||
field.Set(reflect.ValueOf(dirs))
|
||||
}
|
||||
}
|
||||
if err = json.Unmarshal(file, &args); err != nil {
|
||||
fmt.Println("config file parse failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
fmt.Printf("config file loaded: %s\n", path)
|
||||
return &args
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+15
-17
@@ -6,12 +6,11 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
@@ -32,29 +31,28 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var storageDir string
|
||||
var dir string
|
||||
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||
storageDir = *args.StorageDir
|
||||
dir = *args.StorageDir
|
||||
} else {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
storageDir = filepath.Join(filepath.Dir(exe), "storage")
|
||||
dir = filepath.Dir(exe)
|
||||
}
|
||||
|
||||
cfg := &model.StartConfig{
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: storageDir,
|
||||
WhiteDownloadDirs: args.WhiteDownloadDirs,
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
WebAuth: webAuth,
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: filepath.Join(dir, "storage"),
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
WebAuth: webAuth,
|
||||
}
|
||||
cmd.Start(cfg)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
module github.com/GopeedLab/gopeed
|
||||
|
||||
go 1.24.9
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.11
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/anacrolix/torrent v1.60.1-0.20251217073903-486bcbe758e0
|
||||
github.com/anacrolix/missinggo/v2 v2.8.0
|
||||
github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/bodgit/sevenzip v1.6.1
|
||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
|
||||
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05
|
||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||
github.com/go-git/go-git/v5 v5.8.1
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
@@ -16,150 +16,137 @@ require (
|
||||
github.com/imroc/req/v3 v3.52.2
|
||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||
github.com/mattn/go-ieproxy v0.0.12
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
|
||||
go.etcd.io/bbolt v1.3.11
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||
golang.org/x/sync v0.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/anacrolix/btree v0.1.1 // indirect
|
||||
github.com/anacrolix/missinggo/v2 v2.10.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/felixge/fgprof v0.9.5 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/icholy/digest v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/monkeyWie/goed2k v0.0.0-20260317100435-7a7575cf2447 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.51.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.1 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1
|
||||
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.9.4 // 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.7.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.23.0 // indirect
|
||||
github.com/anacrolix/envpprof v1.5.0 // indirect
|
||||
github.com/anacrolix/generics v0.2.0 // indirect
|
||||
github.com/anacrolix/go-libutp v1.3.2 // indirect
|
||||
github.com/anacrolix/log v0.17.1-0.20251118025802-918f1157b7bb // indirect
|
||||
github.com/anacrolix/chansync v0.6.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.22.0 // indirect
|
||||
github.com/anacrolix/envpprof v1.4.0 // indirect
|
||||
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
||||
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||
github.com/anacrolix/log v0.16.0 // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||
github.com/anacrolix/mmsg v1.1.1 // indirect
|
||||
github.com/anacrolix/multiless v0.4.0 // indirect
|
||||
github.com/anacrolix/stm v0.5.0 // indirect
|
||||
github.com/anacrolix/sync v0.6.0 // indirect
|
||||
github.com/anacrolix/sync v0.5.3 // indirect
|
||||
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||
github.com/anacrolix/utp v0.2.0 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/edsrzf/mmap-go v1.2.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-llsqlite/adapter v0.2.0 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||
github.com/go-llsqlite/crawshaw v0.5.5 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-varint v0.1.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pion/datachannel v1.6.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||
github.com/pion/ice/v4 v4.2.0 // indirect
|
||||
github.com/pion/interceptor v0.1.42 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pion/datachannel v1.5.9 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||
github.com/pion/interceptor v0.1.37 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.16 // indirect
|
||||
github.com/pion/rtp v1.10.0 // indirect
|
||||
github.com/pion/sctp v1.9.1 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.17 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.10 // indirect
|
||||
github.com/pion/stun/v3 v3.1.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.4 // indirect
|
||||
github.com/pion/webrtc/v4 v4.2.2-0.20260109001657-a5962f314db7 // indirect
|
||||
github.com/pion/rtcp v1.2.14 // indirect
|
||||
github.com/pion/rtp v1.8.9 // indirect
|
||||
github.com/pion/sctp v1.8.33 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pion/webrtc/v4 v4.0.2 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.12
|
||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/tidwall/btree v1.8.1 // indirect
|
||||
github.com/tidwall/btree v1.7.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
modernc.org/libc v1.67.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.43.0 // indirect
|
||||
zombiezen.com/go/sqlite v1.4.2 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
modernc.org/libc v1.61.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.33.1 // indirect
|
||||
zombiezen.com/go/sqlite v1.4.0 // indirect
|
||||
)
|
||||
@@ -1,23 +1,21 @@
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Fetcher defines the interface for a download protocol.
|
||||
// Each download task will have a corresponding Fetcher instance for the management of the download task
|
||||
type Fetcher interface {
|
||||
Setup(ctl *controller.Controller)
|
||||
Resolve(req *base.Request, opts *base.Options) error
|
||||
// Resolve resource info from request
|
||||
Resolve(req *base.Request) error
|
||||
// Create ready to download, but not started
|
||||
Create(opts *base.Options) error
|
||||
Start() error
|
||||
// Patch modifies task-specific data based on the protocol.
|
||||
// For HTTP: can modify Request info (URL, headers, etc.)
|
||||
// For BT: can modify SelectFiles (via opts.SelectFiles)
|
||||
Patch(req *base.Request, opts *base.Options) error
|
||||
Pause() error
|
||||
Close() error
|
||||
|
||||
@@ -127,21 +125,6 @@ type FetcherManager interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// StatefulFetcherManager is an optional extension for protocols that keep
|
||||
// shared client state outside individual task fetchers.
|
||||
type StatefulFetcherManager interface {
|
||||
SetStateStore(store ProtocolStateStore)
|
||||
}
|
||||
|
||||
// ProtocolStateStore persists shared protocol state for a fetcher manager.
|
||||
// Downloader provides the concrete storage backend, while the protocol decides
|
||||
// when state should be loaded or flushed.
|
||||
type ProtocolStateStore interface {
|
||||
Load(v any) (bool, error)
|
||||
Save(v any) error
|
||||
Delete() error
|
||||
}
|
||||
|
||||
type DefaultFetcher struct {
|
||||
Ctl *controller.Controller
|
||||
Meta *FetcherMeta
|
||||
|
||||
@@ -20,15 +20,15 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *torrent.ClientConfig
|
||||
client *torrent.Client
|
||||
lock sync.Mutex
|
||||
closeCtx context.Context
|
||||
closeFunc func()
|
||||
client *torrent.Client
|
||||
lock sync.Mutex
|
||||
torrentDirMap = make(map[string]string)
|
||||
ftMap = make(map[string]*fileTorrentImpl)
|
||||
closeCtx context.Context
|
||||
closeFunc func()
|
||||
)
|
||||
|
||||
type Fetcher struct {
|
||||
@@ -71,12 +71,21 @@ func (f *Fetcher) initClient() (err error) {
|
||||
closeCtx, closeFunc = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
cfg = torrent.NewDefaultClientConfig()
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
cfg.Seed = true
|
||||
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||
cfg.ListenPort = f.config.ListenPort
|
||||
cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy)
|
||||
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||
ClientBaseDir: cfg.DataDir,
|
||||
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||
if dir, ok := torrentDirMap[infoHash.String()]; ok {
|
||||
ft.setTorrentDir(dir)
|
||||
}
|
||||
ftMap[infoHash.String()] = ft
|
||||
},
|
||||
})
|
||||
dnsResolver := &DnsCacheResolver{RefreshTimeout: 5 * time.Minute}
|
||||
cfg.TrackerDialContext = dnsResolver.DialContext
|
||||
client, err = torrent.NewClient(cfg)
|
||||
@@ -91,12 +100,8 @@ func (f *Fetcher) initClient() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) error {
|
||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
f.meta.Req = req
|
||||
f.meta.Opts = opts
|
||||
if f.meta.Opts == nil {
|
||||
f.meta.Opts = &base.Options{}
|
||||
}
|
||||
if err := f.addTorrent(req, false); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -104,13 +109,23 @@ func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||
f.meta.Opts = opts
|
||||
if f.meta.Res != nil {
|
||||
torrentDirMap[f.meta.Res.Hash] = opts.Path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) Start() (err error) {
|
||||
if !f.torrentReady.Load() {
|
||||
if err = f.addTorrent(f.meta.Req, false); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||
ft.setTorrentDir(f.meta.Opts.Path)
|
||||
}
|
||||
files := f.torrent.Files()
|
||||
// If the user does not specify the file to download, all files will be downloaded by default
|
||||
if f.data.Progress == nil {
|
||||
@@ -130,12 +145,12 @@ func (f *Fetcher) Start() (err error) {
|
||||
file.Download()
|
||||
}
|
||||
}
|
||||
f.torrent.AllowDataDownload()
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Fetcher) Pause() (err error) {
|
||||
f.torrent.DisallowDataDownload()
|
||||
f.torrentReady.Store(false)
|
||||
f.safeDrop()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -232,59 +247,6 @@ func (f *Fetcher) isDone() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Patch modifies the BT task settings.
|
||||
// Invalid file indices are silently ignored.
|
||||
func (f *Fetcher) Patch(req *base.Request, opts *base.Options) error {
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.SelectFiles != nil {
|
||||
selectFiles := opts.SelectFiles
|
||||
|
||||
// Get file count from resource metadata
|
||||
fileCount := 0
|
||||
if f.meta.Res != nil {
|
||||
fileCount = len(f.meta.Res.Files)
|
||||
}
|
||||
|
||||
// Filter out invalid indices (silently ignore)
|
||||
validSelectFiles := make([]int, 0, len(selectFiles))
|
||||
for _, idx := range selectFiles {
|
||||
if idx >= 0 && idx < fileCount {
|
||||
validSelectFiles = append(validSelectFiles, idx)
|
||||
}
|
||||
}
|
||||
|
||||
if f.torrent != nil {
|
||||
files := f.torrent.Files()
|
||||
|
||||
// Cancel all current file downloads first
|
||||
f.torrent.CancelPieces(0, f.torrent.NumPieces())
|
||||
|
||||
// Apply new file selection
|
||||
if len(validSelectFiles) == len(files) {
|
||||
f.torrent.DownloadAll()
|
||||
} else {
|
||||
for _, selectIndex := range validSelectFiles {
|
||||
file := files[selectIndex]
|
||||
file.Download()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f.meta.Opts.SelectFiles = validSelectFiles
|
||||
// Recalculate the resource size based on new selection
|
||||
if f.meta.Res != nil {
|
||||
f.meta.Res.CalcSize(validSelectFiles)
|
||||
}
|
||||
// Reset progress tracking for new file selection
|
||||
f.data.Progress = make(fetcher.Progress, len(validSelectFiles))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) updateRes() {
|
||||
res := &base.Resource{
|
||||
Range: true,
|
||||
@@ -399,12 +361,8 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||
}
|
||||
schema := util.ParseSchema(req.URL)
|
||||
privateTorrent := false
|
||||
var spec *torrent.TorrentSpec
|
||||
if schema == "MAGNET" {
|
||||
spec, err = torrent.TorrentSpecFromMagnetUri(req.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
f.torrent, err = client.AddMagnet(req.URL)
|
||||
} else {
|
||||
var reader io.Reader
|
||||
if schema == "FILE" {
|
||||
@@ -441,18 +399,8 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||
if info.Private != nil && *info.Private {
|
||||
privateTorrent = true
|
||||
}
|
||||
spec, err = torrent.TorrentSpecFromMetaInfoErr(metaInfo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
f.torrent, err = client.AddTorrent(metaInfo)
|
||||
}
|
||||
spec.Storage = storage.NewFileOpts(storage.NewFileClientOpts{
|
||||
ClientBaseDir: cfg.DataDir,
|
||||
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
|
||||
return f.meta.Opts.Path
|
||||
},
|
||||
})
|
||||
f.torrent, _, err = client.AddTorrentSpec(spec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,18 +3,17 @@ package bt
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
)
|
||||
|
||||
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||
@@ -31,7 +30,7 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
||||
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
|
||||
err = fetcher.Resolve(&base.Request{
|
||||
URL: dataUri,
|
||||
}, nil)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -81,7 +80,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -111,7 +110,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
"udp://tracker.bitsearch.to:1337/announce",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -145,7 +144,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: "./testdata/test.unclean.torrent",
|
||||
}, nil)
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
||||
}
|
||||
@@ -156,7 +155,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||
uri := "file:///" + file
|
||||
err := fetcher.Resolve(&base.Request{
|
||||
URL: uri,
|
||||
}, nil)
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
|
||||
}
|
||||
@@ -239,130 +238,3 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher
|
||||
fetcher.Setup(newController)
|
||||
return fetcher
|
||||
}
|
||||
|
||||
// TestFetcher_Patch tests the Patch functionality for BT fetcher.
|
||||
// It tests modifying selected files after Resolve (without downloading).
|
||||
func TestFetcher_Patch(t *testing.T) {
|
||||
f := buildFetcher()
|
||||
|
||||
// Resolve a multi-file torrent
|
||||
err := f.Resolve(&base.Request{
|
||||
URL: "./testdata/test.torrent",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify initial state: 3 files, all selected by default
|
||||
meta := f.Meta()
|
||||
if len(meta.Res.Files) != 3 {
|
||||
t.Fatalf("Expected 3 files, got %d", len(meta.Res.Files))
|
||||
}
|
||||
if len(meta.Opts.SelectFiles) != 3 {
|
||||
t.Fatalf("Expected 3 selected files, got %d", len(meta.Opts.SelectFiles))
|
||||
}
|
||||
|
||||
// Total size of all files: 107484864
|
||||
totalSize := int64(107484864)
|
||||
if meta.Res.Size != totalSize {
|
||||
t.Fatalf("Expected total size %d, got %d", totalSize, meta.Res.Size)
|
||||
}
|
||||
|
||||
t.Run("Patch with valid indices", func(t *testing.T) {
|
||||
// Select only file 0 and 2 (c.txt and a.txt)
|
||||
err := f.Patch(nil, &base.Options{
|
||||
SelectFiles: []int{0, 2},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Patch failed: %v", err)
|
||||
}
|
||||
|
||||
meta := f.Meta()
|
||||
if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 2}) {
|
||||
t.Errorf("Expected SelectFiles [0, 2], got %v", meta.Opts.SelectFiles)
|
||||
}
|
||||
|
||||
// Size should be recalculated: c.txt (98501754) + a.txt (78114) = 98579868
|
||||
expectedSize := int64(98501754 + 78114)
|
||||
if meta.Res.Size != expectedSize {
|
||||
t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Patch with invalid indices are silently ignored", func(t *testing.T) {
|
||||
// Mix of valid (0, 1) and invalid (-1, 5, 100) indices
|
||||
err := f.Patch(nil, &base.Options{
|
||||
SelectFiles: []int{-1, 0, 5, 1, 100},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Patch should not return error for invalid indices: %v", err)
|
||||
}
|
||||
|
||||
meta := f.Meta()
|
||||
// Only valid indices 0 and 1 should remain
|
||||
if !reflect.DeepEqual(meta.Opts.SelectFiles, []int{0, 1}) {
|
||||
t.Errorf("Expected SelectFiles [0, 1], got %v", meta.Opts.SelectFiles)
|
||||
}
|
||||
|
||||
// Size should be: c.txt (98501754) + b.txt (8904996) = 107406750
|
||||
expectedSize := int64(98501754 + 8904996)
|
||||
if meta.Res.Size != expectedSize {
|
||||
t.Errorf("Expected size %d, got %d", expectedSize, meta.Res.Size)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Patch with all invalid indices results in empty selection", func(t *testing.T) {
|
||||
err := f.Patch(nil, &base.Options{
|
||||
SelectFiles: []int{-5, 10, 999},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Patch should not return error: %v", err)
|
||||
}
|
||||
|
||||
meta := f.Meta()
|
||||
if len(meta.Opts.SelectFiles) != 0 {
|
||||
t.Errorf("Expected empty SelectFiles, got %v", meta.Opts.SelectFiles)
|
||||
}
|
||||
|
||||
// Note: CalcSize with empty selectFiles calculates total size of all files
|
||||
// This is by design - empty selection in CalcSize means "all files"
|
||||
// But SelectFiles being empty means no files are selected for download
|
||||
if meta.Res.Size != totalSize {
|
||||
t.Errorf("Expected size %d (CalcSize with empty slice = all files), got %d", totalSize, meta.Res.Size)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Patch with nil opts does nothing", func(t *testing.T) {
|
||||
// First set a known state
|
||||
f.Patch(nil, &base.Options{SelectFiles: []int{1}})
|
||||
prevSelectFiles := f.Meta().Opts.SelectFiles
|
||||
|
||||
// Patch with nil opts
|
||||
err := f.Patch(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Patch with nil opts should not fail: %v", err)
|
||||
}
|
||||
|
||||
// Should remain unchanged
|
||||
if !reflect.DeepEqual(f.Meta().Opts.SelectFiles, prevSelectFiles) {
|
||||
t.Errorf("SelectFiles should remain unchanged after nil opts Patch")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Patch progress array is resized", func(t *testing.T) {
|
||||
btFetcher := f.(*Fetcher)
|
||||
// Initialize progress array
|
||||
btFetcher.data.Progress = make(fetcher.Progress, 3)
|
||||
|
||||
err := f.Patch(nil, &base.Options{
|
||||
SelectFiles: []int{0, 2},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Patch failed: %v", err)
|
||||
}
|
||||
|
||||
if len(btFetcher.data.Progress) != 2 {
|
||||
t.Errorf("Expected Progress length 2, got %d", len(btFetcher.data.Progress))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// modify from github.com/anacrolix/torrent/storage/file.go
|
||||
|
||||
package bt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/anacrolix/missinggo/v2"
|
||||
|
||||
"github.com/anacrolix/torrent/common"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/anacrolix/torrent/segments"
|
||||
)
|
||||
|
||||
// File-based storage for torrents, that isn't yet bound to a particular torrent.
|
||||
type fileClientImpl struct {
|
||||
opts newFileClientOpts
|
||||
}
|
||||
|
||||
type newFileClientOpts struct {
|
||||
// The base directory for all downloads.
|
||||
ClientBaseDir string
|
||||
HandleFileTorrent func(infoHash metainfo.Hash, ft *fileTorrentImpl)
|
||||
FilePathMaker storage.FilePathMaker
|
||||
PieceCompletion storage.PieceCompletion
|
||||
}
|
||||
|
||||
func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
|
||||
if opts.FilePathMaker == nil {
|
||||
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
||||
var parts []string
|
||||
if opts.Info.Name != metainfo.NoName {
|
||||
parts = append(parts, opts.Info.Name)
|
||||
}
|
||||
return filepath.Join(append(parts, opts.File.Path...)...)
|
||||
}
|
||||
}
|
||||
if opts.PieceCompletion == nil {
|
||||
ret, err := storage.NewDefaultPieceCompletionForDir(opts.ClientBaseDir)
|
||||
if err != nil {
|
||||
ret = storage.NewMapPieceCompletion()
|
||||
}
|
||||
opts.PieceCompletion = ret
|
||||
}
|
||||
return fileClientImpl{opts}
|
||||
}
|
||||
|
||||
func (fs fileClientImpl) Close() error {
|
||||
return fs.opts.PieceCompletion.Close()
|
||||
}
|
||||
|
||||
func (fs fileClientImpl) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
||||
upvertedFiles := info.UpvertedFiles()
|
||||
files := make([]file, 0, len(upvertedFiles))
|
||||
for _, fileInfo := range upvertedFiles {
|
||||
filePath := filepath.Join("", fs.opts.FilePathMaker(storage.FilePathMakerOpts{
|
||||
Info: info,
|
||||
File: &fileInfo,
|
||||
}))
|
||||
f := file{
|
||||
rawPath: filePath,
|
||||
path: filePath,
|
||||
length: fileInfo.Length,
|
||||
}
|
||||
if f.length == 0 {
|
||||
err = CreateNativeZeroLengthFile(f.path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("creating zero length file: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
t := &fileTorrentImpl{
|
||||
files,
|
||||
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
|
||||
infoHash,
|
||||
fs.opts.PieceCompletion,
|
||||
}
|
||||
fs.opts.HandleFileTorrent(infoHash, t)
|
||||
return storage.TorrentImpl{
|
||||
Piece: t.Piece,
|
||||
Close: t.Close,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type file struct {
|
||||
// The safe, OS-local file path.
|
||||
rawPath string
|
||||
path string
|
||||
length int64
|
||||
}
|
||||
|
||||
type fileTorrentImpl struct {
|
||||
files []file
|
||||
segmentLocater segments.Index
|
||||
infoHash metainfo.Hash
|
||||
completion storage.PieceCompletion
|
||||
}
|
||||
|
||||
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
|
||||
// Create a view onto the file-based torrent storage.
|
||||
_io := fileTorrentImplIO{fts}
|
||||
// Return the appropriate segments of this.
|
||||
return &filePieceImpl{
|
||||
fts,
|
||||
p,
|
||||
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
|
||||
io.NewSectionReader(_io, p.Offset(), p.Length()),
|
||||
}
|
||||
}
|
||||
|
||||
func (fts *fileTorrentImpl) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fts *fileTorrentImpl) setTorrentDir(dir string) {
|
||||
for i, f := range fts.files {
|
||||
fts.files[i].path = filepath.Join(dir, f.rawPath)
|
||||
}
|
||||
}
|
||||
|
||||
// A helper to create zero-length files which won't appear for file-orientated storage since no
|
||||
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
|
||||
// caller should make sure the file name provided is safe/sanitized.
|
||||
func CreateNativeZeroLengthFile(name string) error {
|
||||
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||
var f io.Closer
|
||||
f, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
|
||||
type fileTorrentImplIO struct {
|
||||
fts *fileTorrentImpl
|
||||
}
|
||||
|
||||
// Returns EOF on short or missing file.
|
||||
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
|
||||
f, err := os.Open(file.path)
|
||||
if os.IsNotExist(err) {
|
||||
// File missing is treated the same as a short file.
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
// Limit the read to within the expected bounds of this file.
|
||||
if int64(len(b)) > file.length-off {
|
||||
b = b[:file.length-off]
|
||||
}
|
||||
for off < file.length && len(b) != 0 {
|
||||
n1, err1 := f.ReadAt(b, off)
|
||||
b = b[n1:]
|
||||
n += n1
|
||||
off += int64(n1)
|
||||
if n1 == 0 {
|
||||
err = err1
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
|
||||
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
|
||||
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
|
||||
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
|
||||
n += n1
|
||||
b = b[n1:]
|
||||
err = err1
|
||||
return err == nil // && int64(n1) == e.Length
|
||||
})
|
||||
if len(b) != 0 && err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
// log.Printf("write at %v: %v bytes", off, len(p))
|
||||
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
|
||||
name := fst.fts.files[i].path
|
||||
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||
var f *os.File
|
||||
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var n1 int
|
||||
n1, err = f.WriteAt(p[:e.Length], e.Start)
|
||||
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
|
||||
closeErr := f.Close()
|
||||
n += n1
|
||||
p = p[n1:]
|
||||
if err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err == nil && int64(n1) != e.Length {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
return err == nil
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// modify from github.com/anacrolix/torrent/storage/file_misc.go
|
||||
|
||||
package bt
|
||||
|
||||
import "github.com/anacrolix/torrent/metainfo"
|
||||
|
||||
type requiredLength struct {
|
||||
fileIndex int
|
||||
length int64
|
||||
}
|
||||
|
||||
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
for i, fi := range info.UpvertedFiles() {
|
||||
if off >= fi.Length {
|
||||
off -= fi.Length
|
||||
continue
|
||||
}
|
||||
n1 := n
|
||||
if off+n1 > fi.Length {
|
||||
n1 = fi.Length - off
|
||||
}
|
||||
ret = append(ret, requiredLength{
|
||||
fileIndex: i,
|
||||
length: off + n1,
|
||||
})
|
||||
n -= n1
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
off = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// modify from github.com/anacrolix/torrent/storage/file_piece.go
|
||||
|
||||
package bt
|
||||
|
||||
import (
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type filePieceImpl struct {
|
||||
*fileTorrentImpl
|
||||
p metainfo.Piece
|
||||
io.WriterAt
|
||||
io.ReaderAt
|
||||
}
|
||||
|
||||
var _ storage.PieceImpl = (*filePieceImpl)(nil)
|
||||
|
||||
func (fs *filePieceImpl) pieceKey() metainfo.PieceKey {
|
||||
return metainfo.PieceKey{InfoHash: fs.infoHash, Index: fs.p.Index()}
|
||||
}
|
||||
|
||||
func (fs *filePieceImpl) Completion() storage.Completion {
|
||||
c, err := fs.completion.Get(fs.pieceKey())
|
||||
if err != nil {
|
||||
log.Printf("error getting piece completion: %s", err)
|
||||
c.Ok = false
|
||||
return c
|
||||
}
|
||||
|
||||
verified := true
|
||||
if c.Complete {
|
||||
// If it's allegedly complete, check that its constituent files have the necessary length.
|
||||
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
|
||||
s, err := os.Stat(fs.files[fi.fileIndex].path)
|
||||
if err != nil || s.Size() < fi.length {
|
||||
verified = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !verified {
|
||||
// The completion was wrong, fix it.
|
||||
c.Complete = false
|
||||
fs.completion.Set(fs.pieceKey(), false)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (fs *filePieceImpl) MarkComplete() error {
|
||||
return fs.completion.Set(fs.pieceKey(), true)
|
||||
}
|
||||
|
||||
func (fs *filePieceImpl) MarkNotComplete() error {
|
||||
return fs.completion.Set(fs.pieceKey(), false)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package ed2k
|
||||
|
||||
const (
|
||||
defaultServerList = "45.82.80.155:5687,176.123.5.89:4725,85.121.5.137:4232,176.123.2.239:4232,145.239.2.134:4661,91.208.162.87:4232,37.15.61.236:4232"
|
||||
defaultServerMet = "ed2k://|serverlist|http://upd.emule-security.org/server.met|/"
|
||||
defaultNodesDat = "https://upd.emule-security.org/nodes.dat"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
ListenPort int `json:"listenPort"`
|
||||
UDPPort int `json:"udpPort"`
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
ServerMet string `json:"serverMet"`
|
||||
NodesDat string `json:"nodesDat"`
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,42 +0,0 @@
|
||||
package ed2k
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
const testLink = "ed2k://|file|cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso|4630972416|8867C5E54405FF9452225B66EFEE690A|/"
|
||||
|
||||
func TestFetcher_Resolve(t *testing.T) {
|
||||
f := (&FetcherManager{}).Build()
|
||||
f.Setup(controller.NewController())
|
||||
|
||||
err := f.Resolve(&base.Request{URL: testLink}, &base.Options{Path: t.TempDir()})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
|
||||
meta := f.Meta()
|
||||
if meta.Res == nil {
|
||||
t.Fatal("Resolve() resource is nil")
|
||||
}
|
||||
if got, want := meta.Res.Hash, "8867C5E54405FF9452225B66EFEE690A"; got != want {
|
||||
t.Fatalf("Resolve() hash = %s, want %s", got, want)
|
||||
}
|
||||
if got, want := meta.Res.Size, int64(4630972416); got != want {
|
||||
t.Fatalf("Resolve() size = %d, want %d", got, want)
|
||||
}
|
||||
if got, want := meta.Res.Files[0].Name, "cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso"; got != want {
|
||||
t.Fatalf("Resolve() name = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetcherManager_ParseName(t *testing.T) {
|
||||
got := (&FetcherManager{}).ParseName(testLink)
|
||||
want := "cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso"
|
||||
if got != want {
|
||||
t.Fatalf("ParseName() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
+564
-1620
File diff suppressed because it is too large.
Load diff
@@ -1,105 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Fetcher Data (for Store/Restore)
|
||||
// ============================================================================
|
||||
|
||||
type fetcherData struct {
|
||||
Connections []*connection
|
||||
RedirectURL string // Saved redirect URL for resume
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fetcher Manager
|
||||
// ============================================================================
|
||||
|
||||
type FetcherManager struct {
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Name() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
|
||||
return []*fetcher.SchemeFilter{
|
||||
{
|
||||
Type: fetcher.FilterTypeUrl,
|
||||
Pattern: "HTTP",
|
||||
},
|
||||
{
|
||||
Type: fetcher.FilterTypeUrl,
|
||||
Pattern: "HTTPS",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Build() fetcher.Fetcher {
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) ParseName(u string) string {
|
||||
var name string
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
name = path.Base(url.Path)
|
||||
if name == "" || name == "/" || name == "." {
|
||||
name = url.Hostname()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) AutoRename() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) DefaultConfig() any {
|
||||
return &config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
Connections: 16,
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||
_f := f.(*Fetcher)
|
||||
_f.redirectLock.Lock()
|
||||
redirectURL := _f.redirectURL
|
||||
_f.redirectLock.Unlock()
|
||||
return &fetcherData{
|
||||
Connections: _f.connections,
|
||||
RedirectURL: redirectURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||
fd := v.(*fetcherData)
|
||||
fb := &FetcherManager{}
|
||||
fetcher := fb.Build().(*Fetcher)
|
||||
fetcher.meta = meta
|
||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||
base.ParseOptExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||
if len(fd.Connections) > 0 {
|
||||
fetcher.connections = fd.Connections
|
||||
}
|
||||
// Restore redirect URL for resume
|
||||
if fd.RedirectURL != "" {
|
||||
fetcher.redirectURL = fd.RedirectURL
|
||||
}
|
||||
return fetcher
|
||||
}
|
||||
}
|
||||
|
||||
func (fm *FetcherManager) Close() error {
|
||||
return nil
|
||||
}
|
||||
+106
-1173
File diff suppressed because it is too large.
Load diff
@@ -1,138 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestParseFilenameWithAmpersand tests the fix for filenames containing & character
|
||||
// Issue: filenames with & are HTML-encoded as & and then truncated at the semicolon
|
||||
func TestParseFilenameWithAmpersand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
disposition string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "quoted filename with &",
|
||||
disposition: `attachment; filename="查询处理&优化.pptx"`,
|
||||
want: "查询处理&优化.pptx",
|
||||
},
|
||||
{
|
||||
name: "unquoted filename with &",
|
||||
disposition: `attachment; filename=test&file.txt`,
|
||||
want: "test&file.txt",
|
||||
},
|
||||
{
|
||||
name: "quoted filename with & and extra params",
|
||||
disposition: `attachment; filename="test&file.txt"; charset=utf-8`,
|
||||
want: "test&file.txt",
|
||||
},
|
||||
{
|
||||
name: "filename with multiple HTML entities",
|
||||
disposition: `attachment; filename="test&<>.txt"`,
|
||||
want: "test&<>.txt",
|
||||
},
|
||||
{
|
||||
name: "normal filename without entities",
|
||||
disposition: `attachment; filename="normal.txt"`,
|
||||
want: "normal.txt",
|
||||
},
|
||||
{
|
||||
name: "filename with actual ampersand (no encoding)",
|
||||
disposition: `attachment; filename="test&file.txt"`,
|
||||
want: "test&file.txt",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseFilename(tt.disposition)
|
||||
if got != tt.want {
|
||||
t.Errorf("parseFilename() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindParamValueEnd tests the helper function for finding parameter value boundaries
|
||||
func TestFindParamValueEnd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "quoted value with semicolon inside",
|
||||
value: `"test&file.txt"; charset=utf-8`,
|
||||
want: 19, // Position of semicolon after closing quote
|
||||
},
|
||||
{
|
||||
name: "quoted value without semicolon after",
|
||||
value: `"test.txt"`,
|
||||
want: -1,
|
||||
},
|
||||
{
|
||||
name: "unquoted value with semicolon",
|
||||
value: `test.txt; charset=utf-8`,
|
||||
want: 8,
|
||||
},
|
||||
{
|
||||
name: "unquoted value without semicolon",
|
||||
value: `test.txt`,
|
||||
want: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := findParamValueEnd(tt.value)
|
||||
if got != tt.want {
|
||||
t.Errorf("findParamValueEnd() = %d, want %d", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnescapeHTMLEntities tests HTML entity unescaping
|
||||
func TestUnescapeHTMLEntities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ampersand",
|
||||
in: "test&file.txt",
|
||||
want: "test&file.txt",
|
||||
},
|
||||
{
|
||||
name: "less than and greater than",
|
||||
in: "<test>.txt",
|
||||
want: "<test>.txt",
|
||||
},
|
||||
{
|
||||
name: "quote",
|
||||
in: "test"file.txt",
|
||||
want: "test\"file.txt",
|
||||
},
|
||||
{
|
||||
name: "multiple entities",
|
||||
in: "a&b<c>d"e",
|
||||
want: "a&b<c>d\"e",
|
||||
},
|
||||
{
|
||||
name: "no entities",
|
||||
in: "normal.txt",
|
||||
want: "normal.txt",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := unescapeHTMLEntities(tt.in)
|
||||
if got != tt.want {
|
||||
t.Errorf("unescapeHTMLEntities() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
type RequestError struct {
|
||||
Code int
|
||||
}
|
||||
|
||||
func NewRequestError(code int) *RequestError {
|
||||
return &RequestError{Code: code}
|
||||
}
|
||||
|
||||
func (re *RequestError) Error() string {
|
||||
return fmt.Sprintf("http request fail, code:%d", re.Code)
|
||||
}
|
||||
|
||||
func isFailureExemptHTTPCode(code int) bool {
|
||||
if code >= 500 && code <= 599 {
|
||||
return true
|
||||
}
|
||||
|
||||
switch code {
|
||||
case 429, 408, 440, 499:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func shouldCountHTTPFailure(err error) bool {
|
||||
var re *RequestError
|
||||
if !errors.As(err, &re) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !isFailureExemptHTTPCode(re.Code)
|
||||
}
|
||||
|
||||
func extractRequestError(err error) *RequestError {
|
||||
var re *RequestError
|
||||
if errors.As(err, &re) {
|
||||
return re
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildRequest creates an HTTP request using the redirect URL if available.
|
||||
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
||||
return f.buildRequestWithURL(ctx, req, true)
|
||||
}
|
||||
|
||||
// buildRequestWithOriginalURL creates an HTTP request using the original URL.
|
||||
// This is used for retrying when the redirect URL has expired.
|
||||
func (f *Fetcher) buildRequestWithOriginalURL(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
||||
return f.buildRequestWithURL(ctx, req, false)
|
||||
}
|
||||
|
||||
// buildRequestWithURL creates an HTTP request.
|
||||
// If useRedirect is true and a redirect URL exists, it will be used; otherwise the original URL is used.
|
||||
func (f *Fetcher) buildRequestWithURL(ctx context.Context, req *base.Request, useRedirect bool) (httpReq *http.Request, err error) {
|
||||
var reqUrl string
|
||||
f.redirectLock.Lock()
|
||||
if useRedirect && f.redirectURL != "" {
|
||||
reqUrl = f.redirectURL
|
||||
} else {
|
||||
reqUrl = req.URL
|
||||
}
|
||||
f.redirectLock.Unlock()
|
||||
|
||||
var (
|
||||
method string
|
||||
body io.Reader
|
||||
)
|
||||
headers := http.Header{}
|
||||
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.Set(k, strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
if extra.Body != "" {
|
||||
body = bytes.NewBufferString(extra.Body)
|
||||
}
|
||||
}
|
||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||
headers.Set(base.HttpHeaderUserAgent, strings.TrimSpace(f.config.UserAgent))
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
httpReq, err = http.NewRequestWithContext(ctx, method, reqUrl, body)
|
||||
} else {
|
||||
httpReq, err = http.NewRequest(method, reqUrl, body)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header = headers
|
||||
if host := headers.Get(base.HttpHeaderHost); host != "" {
|
||||
httpReq.Host = host
|
||||
}
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
// updateRedirectURL updates the redirect URL from the response.
|
||||
// This is called when a request using the original URL succeeds after the redirect URL expired.
|
||||
func (f *Fetcher) updateRedirectURL(url string) {
|
||||
f.redirectLock.Lock()
|
||||
f.redirectURL = url
|
||||
f.redirectLock.Unlock()
|
||||
}
|
||||
|
||||
// hasRedirectURL checks if a redirect URL exists and is different from the original URL.
|
||||
func (f *Fetcher) hasRedirectURL() bool {
|
||||
f.redirectLock.Lock()
|
||||
defer f.redirectLock.Unlock()
|
||||
return f.redirectURL != "" && f.redirectURL != f.meta.Req.URL
|
||||
}
|
||||
|
||||
// isRedirectExpiredError checks if the error indicates that the redirect URL may have expired.
|
||||
// This includes 403 (Forbidden), 401 (Unauthorized), 410 (Gone), and network errors.
|
||||
func isRedirectExpiredError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for specific HTTP error codes that might indicate URL expiration
|
||||
if re := extractRequestError(err); re != nil {
|
||||
switch re.Code {
|
||||
case 401, 403, 404, 410:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// tryFallbackToOriginalURL attempts to make a request using the original URL
|
||||
// when the redirect URL has expired. Returns the response if successful.
|
||||
func (f *Fetcher) tryFallbackToOriginalURL(ctx context.Context, client *http.Client, rangeStart, rangeEnd int64) (*http.Response, error) {
|
||||
httpReq, err := f.buildRequestWithOriginalURL(ctx, f.meta.Req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if f.meta.Res.Range && rangeEnd > 0 {
|
||||
httpReq.Header.Set(base.HttpHeaderRange,
|
||||
fmt.Sprintf(base.HttpHeaderRangeFormat, rangeStart, rangeEnd))
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||
resp.Body.Close()
|
||||
return nil, NewRequestError(resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// buildClient creates an HTTP client with the default connection timeout.
|
||||
// Used for resolve phase where we don't have connection time data yet.
|
||||
func (f *Fetcher) buildClient() *http.Client {
|
||||
return f.buildClientWithTimeout(connectTimeout)
|
||||
}
|
||||
|
||||
// buildFastFailClient creates an HTTP client with fast-fail timeout.
|
||||
// Uses max(minFastFailTimeout, maxConnTime) for fast-fail retry during download phase.
|
||||
func (f *Fetcher) buildFastFailClient() *http.Client {
|
||||
maxConn := f.maxConnTime.Load()
|
||||
if maxConn == 0 {
|
||||
// No successful connection yet, use default timeout
|
||||
return f.buildClientWithTimeout(connectTimeout)
|
||||
}
|
||||
|
||||
timeout := maxConn
|
||||
if timeout >= minFastFailTimeout {
|
||||
// If greater than minFastFailTimeout, increase by 50% for safety margin
|
||||
timeout = int64(float64(timeout) * 1.5)
|
||||
} else {
|
||||
timeout = minFastFailTimeout
|
||||
}
|
||||
return f.buildClientWithTimeout(time.Duration(timeout))
|
||||
}
|
||||
|
||||
// buildClientWithTimeout creates an HTTP client with the specified connection timeout.
|
||||
func (f *Fetcher) buildClientWithTimeout(timeout time.Duration) *http.Client {
|
||||
transport := &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: timeout,
|
||||
}).DialContext,
|
||||
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
||||
},
|
||||
TLSHandshakeTimeout: timeout,
|
||||
}
|
||||
jar, _ := cookiejar.New(nil)
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Jar: jar,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Filename Parsing
|
||||
// ============================================================================
|
||||
|
||||
// parseFilename extracts filename from Content-Disposition header
|
||||
func parseFilename(contentDisposition string) string {
|
||||
// Try RFC 5987 extended notation first (filename*=)
|
||||
if filename := parseFilenameExtended(contentDisposition); filename != "" {
|
||||
return filename
|
||||
}
|
||||
|
||||
// Try standard MIME parsing
|
||||
_, params, err := mime.ParseMediaType(contentDisposition)
|
||||
if err == nil {
|
||||
if filename := params["filename"]; filename != "" {
|
||||
return decodeFilenameParam(filename)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to manual parsing
|
||||
return parseFilenameFallback(contentDisposition)
|
||||
}
|
||||
|
||||
// parseFilenameExtended handles RFC 5987 extended notation (filename*=)
|
||||
// Format: filename*=charset'language'value (e.g., UTF-8”%E6%B5%8B%E8%AF%95.zip)
|
||||
func parseFilenameExtended(cd string) string {
|
||||
lower := strings.ToLower(cd)
|
||||
idx := strings.Index(lower, "filename*=")
|
||||
if idx == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
value := cd[idx+len("filename*="):]
|
||||
|
||||
// Find the end of the value using proper quote handling
|
||||
endIdx := findParamValueEnd(value)
|
||||
if endIdx != -1 {
|
||||
value = value[:endIdx]
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
// Try charset''encoded format (e.g., UTF-8''%E4%B8%AD%E6%96%87.txt)
|
||||
parts := strings.SplitN(value, "''", 2)
|
||||
if len(parts) == 2 {
|
||||
// Use PathUnescape to handle %2B correctly (should decode to +, not space)
|
||||
decoded, err := url.PathUnescape(parts[1])
|
||||
if err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
|
||||
// Try charset'language'encoded format
|
||||
parts = strings.SplitN(value, "'", 3)
|
||||
if len(parts) >= 3 {
|
||||
// Use PathUnescape to handle %2B correctly (should decode to +, not space)
|
||||
decoded, err := url.PathUnescape(parts[2])
|
||||
if err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// decodeFilenameParam decodes filename parameter value
|
||||
// Handles HTML entities, MIME encoded-word, URL encoding, and GBK encoding fallback
|
||||
func decodeFilenameParam(filename string) string {
|
||||
// First, unescape HTML entities (e.g., & -> &, < -> <, > -> >)
|
||||
// This must be done before other decoding to handle cases where servers
|
||||
// HTML-encode special characters in filenames
|
||||
filename = unescapeHTMLEntities(filename)
|
||||
|
||||
// Handle RFC 2047 encoded word (=?charset?encoding?text?=)
|
||||
if strings.HasPrefix(filename, "=?") {
|
||||
decoder := new(mime.WordDecoder)
|
||||
normalizedFilename := strings.Replace(filename, "UTF8", "UTF-8", 1)
|
||||
if decoded, err := decoder.Decode(normalizedFilename); err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
|
||||
// Try URL decoding - use PathUnescape for filenames to handle %2B correctly
|
||||
decoded := util.TryUrlPathUnescape(filename)
|
||||
|
||||
// If not valid UTF-8, try GBK decoding (common for Chinese websites)
|
||||
if !utf8.ValidString(decoded) {
|
||||
if gbkDecoded := tryDecodeGBK(decoded); gbkDecoded != "" {
|
||||
return gbkDecoded
|
||||
}
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
// unescapeHTMLEntities unescapes common HTML entities in filenames
|
||||
// This handles cases where servers HTML-encode special characters like & to &
|
||||
func unescapeHTMLEntities(s string) string {
|
||||
// Common HTML entities that might appear in filenames
|
||||
replacements := map[string]string{
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
""": "\"",
|
||||
"'": "'",
|
||||
"'": "'",
|
||||
}
|
||||
|
||||
result := s
|
||||
for entity, char := range replacements {
|
||||
result = strings.ReplaceAll(result, entity, char)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// tryDecodeGBK attempts to decode string as GBK encoding
|
||||
func tryDecodeGBK(s string) string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
decoder := simplifiedchinese.GBK.NewDecoder()
|
||||
decoded, err := decoder.Bytes([]byte(s))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
result := string(decoded)
|
||||
if utf8.ValidString(result) {
|
||||
return result
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parseFilenameFallback is a fallback parser for non-standard Content-Disposition
|
||||
func parseFilenameFallback(cd string) string {
|
||||
lower := strings.ToLower(cd)
|
||||
idx := strings.Index(lower, "filename=")
|
||||
if idx == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
value := cd[idx+len("filename="):]
|
||||
|
||||
// Find the end of the value using proper quote handling
|
||||
endIdx := findParamValueEnd(value)
|
||||
if endIdx != -1 {
|
||||
value = value[:endIdx]
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
// Remove surrounding quotes
|
||||
if len(value) >= 2 {
|
||||
if (value[0] == '"' && value[len(value)-1] == '"') ||
|
||||
(value[0] == '\'' && value[len(value)-1] == '\'') {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
}
|
||||
|
||||
return decodeFilenameParam(value)
|
||||
}
|
||||
|
||||
// findParamValueEnd finds the end position of a parameter value in a Content-Disposition header.
|
||||
// It correctly handles quoted values where semicolons inside quotes should not be treated as delimiters.
|
||||
// It also handles HTML entities in unquoted values (e.g., & should not be split at the semicolon).
|
||||
// Returns the end index (exclusive) of the value, or -1 if it extends to the end of the string.
|
||||
func findParamValueEnd(value string) int {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// If the value starts with a quote, find the matching closing quote
|
||||
if value[0] == '"' || value[0] == '\'' {
|
||||
quote := value[0]
|
||||
// Find the closing quote, handling escaped quotes
|
||||
for i := 1; i < len(value); i++ {
|
||||
if value[i] == quote {
|
||||
// Check if it's escaped
|
||||
if i > 0 && value[i-1] == '\\' {
|
||||
continue
|
||||
}
|
||||
// Found closing quote, now look for ; after it
|
||||
remaining := value[i+1:]
|
||||
if semiIdx := strings.Index(remaining, ";"); semiIdx != -1 {
|
||||
return i + 1 + semiIdx
|
||||
}
|
||||
return -1 // No semicolon after closing quote
|
||||
}
|
||||
}
|
||||
// No closing quote found, treat rest of string as value
|
||||
return -1
|
||||
}
|
||||
|
||||
// Unquoted value - find the next semicolon that's not part of an HTML entity
|
||||
// HTML entities have the pattern &...; (e.g., & < > " ')
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] == ';' {
|
||||
// Check if this semicolon is part of an HTML entity
|
||||
// Look backwards for an & character
|
||||
isEntity := false
|
||||
if i > 0 {
|
||||
// Look for & before this semicolon (within reasonable distance, max 10 chars)
|
||||
for j := i - 1; j >= 0 && j >= i-10; j-- {
|
||||
if value[j] == '&' {
|
||||
// Found &, this semicolon might be part of an HTML entity
|
||||
// Check if there are only alphanumeric or # between & and ;
|
||||
entityChars := value[j+1 : i]
|
||||
if len(entityChars) > 0 && isValidHTMLEntityChars(entityChars) {
|
||||
isEntity = true
|
||||
}
|
||||
break
|
||||
}
|
||||
// If we hit whitespace or another special char, stop looking
|
||||
if value[j] == ' ' || value[j] == '"' || value[j] == '\'' {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isEntity {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1 // No semicolon, extends to end
|
||||
}
|
||||
|
||||
// isValidHTMLEntityChars checks if a string contains only valid HTML entity characters
|
||||
// (alphanumeric and #, typically for entities like & < ' etc.)
|
||||
func isValidHTMLEntityChars(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '#') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+15
-570
File diff suppressed because it is too large.
Load diff
+11
-71
@@ -2,14 +2,13 @@ package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/mattn/go-ieproxy"
|
||||
"golang.org/x/exp/slices"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/mattn/go-ieproxy"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Request download request
|
||||
@@ -78,7 +77,7 @@ func (r *Resource) Validate() error {
|
||||
if r.Name == "" {
|
||||
return fmt.Errorf("invalid resource name")
|
||||
}
|
||||
if len(r.Files) == 0 {
|
||||
if r.Files == nil || len(r.Files) == 0 {
|
||||
return fmt.Errorf("invalid resource files")
|
||||
}
|
||||
for _, file := range r.Files {
|
||||
@@ -124,7 +123,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
|
||||
// if selectFiles is empty, select all files
|
||||
if len(o.SelectFiles) == 0 {
|
||||
o.SelectFiles = make([]int, fileSize)
|
||||
for i := range fileSize {
|
||||
for i := 0; i < fileSize; i++ {
|
||||
o.SelectFiles[i] = i
|
||||
}
|
||||
}
|
||||
@@ -149,7 +148,7 @@ func ParseReqExtra[E any](req *Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseOptExtra[E any](opts *Options) error {
|
||||
func ParseOptsExtra[E any](opts *Options) error {
|
||||
if opts.Extra == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -178,16 +177,11 @@ type CreateTaskBatchItem struct {
|
||||
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"`
|
||||
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||
Webhook *WebhookConfig `json:"webhook"` // Webhook is the webhook configuration
|
||||
Script *ScriptConfig `json:"script"` // Script is the script execution configuration
|
||||
AutoTorrent *AutoTorrentConfig `json:"autoTorrent"` // AutoTorrent is the auto torrent task creation configuration
|
||||
Archive *ArchiveConfig `json:"archive"` // Archive is the archive extraction configuration
|
||||
AutoDeleteMissingFileTasks bool `json:"autoDeleteMissingFileTasks"` // AutoDeleteMissingFileTasks enables automatic deletion of tasks with missing files
|
||||
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||
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 {
|
||||
@@ -200,24 +194,6 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = &DownloaderProxyConfig{}
|
||||
}
|
||||
if cfg.Webhook == nil {
|
||||
cfg.Webhook = &WebhookConfig{}
|
||||
}
|
||||
if cfg.Script == nil {
|
||||
cfg.Script = &ScriptConfig{}
|
||||
}
|
||||
if cfg.AutoTorrent == nil {
|
||||
cfg.AutoTorrent = &AutoTorrentConfig{
|
||||
Enable: false,
|
||||
DeleteAfterDownload: false,
|
||||
}
|
||||
}
|
||||
if cfg.Archive == nil {
|
||||
cfg.Archive = &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -240,45 +216,9 @@ func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *Downl
|
||||
if cfg.Proxy == nil {
|
||||
cfg.Proxy = beforeCfg.Proxy
|
||||
}
|
||||
if cfg.Webhook == nil {
|
||||
cfg.Webhook = beforeCfg.Webhook
|
||||
}
|
||||
if cfg.Script == nil {
|
||||
cfg.Script = beforeCfg.Script
|
||||
}
|
||||
if cfg.AutoTorrent == nil {
|
||||
cfg.AutoTorrent = beforeCfg.AutoTorrent
|
||||
}
|
||||
if cfg.Archive == nil {
|
||||
cfg.Archive = beforeCfg.Archive
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// WebhookConfig is the webhook configuration
|
||||
type WebhookConfig struct {
|
||||
Enable bool `json:"enable"` // Enable is the flag to enable/disable webhooks
|
||||
URLs []string `json:"urls"` // URLs is the list of webhook URLs
|
||||
}
|
||||
|
||||
// ScriptConfig is the script execution configuration
|
||||
type ScriptConfig struct {
|
||||
Enable bool `json:"enable"` // Enable is the flag to enable/disable script execution
|
||||
Paths []string `json:"paths"` // Paths is the list of script paths to execute
|
||||
}
|
||||
|
||||
// AutoTorrentConfig is the auto torrent task creation configuration
|
||||
type AutoTorrentConfig struct {
|
||||
Enable bool `json:"enable"` // Enable enables automatic BT task creation when downloading .torrent files
|
||||
DeleteAfterDownload bool `json:"deleteAfterDownload"` // DeleteAfterDownload deletes the .torrent file after BT task creation
|
||||
}
|
||||
|
||||
// ArchiveConfig is the archive extraction configuration
|
||||
type ArchiveConfig struct {
|
||||
AutoExtract bool `json:"autoExtract"` // AutoExtract enables automatic extraction of archives after download
|
||||
DeleteAfterExtract bool `json:"deleteAfterExtract"` // DeleteAfterExtract deletes the archive after successful extraction
|
||||
}
|
||||
|
||||
type DownloaderProxyConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
// System is the flag that use system proxy
|
||||
|
||||
+1
-174
@@ -18,16 +18,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
Webhook: &WebhookConfig{},
|
||||
Script: &ScriptConfig{},
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: false,
|
||||
DeleteAfterDownload: false,
|
||||
},
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -39,16 +29,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
MaxRunning: 10,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
Webhook: &WebhookConfig{},
|
||||
Script: &ScriptConfig{},
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: false,
|
||||
DeleteAfterDownload: false,
|
||||
},
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -63,17 +43,7 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
ProtocolConfig: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
Webhook: &WebhookConfig{},
|
||||
Script: &ScriptConfig{},
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: false,
|
||||
DeleteAfterDownload: false,
|
||||
},
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -89,64 +59,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
Proxy: &DownloaderProxyConfig{
|
||||
Enable: true,
|
||||
},
|
||||
Webhook: &WebhookConfig{},
|
||||
Script: &ScriptConfig{},
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: false,
|
||||
DeleteAfterDownload: false,
|
||||
},
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init AutoTorrent",
|
||||
&DownloaderStoreConfig{
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: true,
|
||||
DeleteAfterDownload: true,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
Webhook: &WebhookConfig{},
|
||||
Script: &ScriptConfig{},
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: true,
|
||||
DeleteAfterDownload: true,
|
||||
},
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Init Archive",
|
||||
&DownloaderStoreConfig{
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: true,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
MaxRunning: 5,
|
||||
ProtocolConfig: map[string]any{},
|
||||
Proxy: &DownloaderProxyConfig{},
|
||||
Webhook: &WebhookConfig{},
|
||||
Script: &ScriptConfig{},
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: false,
|
||||
DeleteAfterDownload: false,
|
||||
},
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: true,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -159,10 +71,6 @@ func TestDownloaderStoreConfig_Init(t *testing.T) {
|
||||
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||
Extra: tt.fields.Extra,
|
||||
Proxy: tt.fields.Proxy,
|
||||
Webhook: tt.fields.Webhook,
|
||||
Script: tt.fields.Script,
|
||||
AutoTorrent: tt.fields.AutoTorrent,
|
||||
Archive: tt.fields.Archive,
|
||||
}
|
||||
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Init() = %v, want %v", got, tt.want)
|
||||
@@ -337,84 +245,6 @@ func TestDownloaderStoreConfig_Merge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge AutoTorrent No Override",
|
||||
&DownloaderStoreConfig{
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: false,
|
||||
DeleteAfterDownload: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge AutoTorrent Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: true,
|
||||
DeleteAfterDownload: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
AutoTorrent: &AutoTorrentConfig{
|
||||
Enable: true,
|
||||
DeleteAfterDownload: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Archive No Override",
|
||||
&DownloaderStoreConfig{
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: true,
|
||||
},
|
||||
},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Merge Archive Override",
|
||||
&DownloaderStoreConfig{},
|
||||
args{
|
||||
beforeCfg: &DownloaderStoreConfig{
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
&DownloaderStoreConfig{
|
||||
Archive: &ArchiveConfig{
|
||||
AutoExtract: false,
|
||||
DeleteAfterExtract: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -425,9 +255,6 @@ func TestDownloaderStoreConfig_Merge(t *testing.T) {
|
||||
ProtocolConfig: tt.fields.ProtocolConfig,
|
||||
Extra: tt.fields.Extra,
|
||||
Proxy: tt.fields.Proxy,
|
||||
Webhook: tt.fields.Webhook,
|
||||
AutoTorrent: tt.fields.AutoTorrent,
|
||||
Archive: tt.fields.Archive,
|
||||
}
|
||||
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Merge() = %v, want %v", got, tt.want)
|
||||
|
||||
+75
-669
File diff suppressed because it is too large.
Load diff
+58
-2147
File diff suppressed because it is too large.
Load diff
@@ -131,7 +131,6 @@ type XMLHttpRequest struct {
|
||||
responseHeaders http.Header
|
||||
aborted bool
|
||||
client *req.Client
|
||||
fingerprint string
|
||||
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||
@@ -165,7 +164,7 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
setFingerprint(xhr.client, xhr.fingerprint)
|
||||
setFingerprint(xhr.client)
|
||||
|
||||
d := xhr.parseData(data)
|
||||
var (
|
||||
@@ -382,8 +381,7 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
||||
}
|
||||
|
||||
instance := &XMLHttpRequest{
|
||||
client: client,
|
||||
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
|
||||
client: client,
|
||||
Upload: &XMLHttpRequestUpload{
|
||||
EventProp: &EventProp{
|
||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||
@@ -397,8 +395,8 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
||||
instanceValue.SetPrototype(call.This.Prototype())
|
||||
return instanceValue
|
||||
})
|
||||
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
|
||||
runtime.Set(FingerprintMagicKey, fingerprint)
|
||||
runtime.Set("__gopeed_setFingerprint", func(fingerprint Fingerprint) {
|
||||
currentFingerprint = fingerprint
|
||||
})
|
||||
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package xhr
|
||||
|
||||
import "github.com/imroc/req/v3"
|
||||
import (
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
type Fingerprint string
|
||||
|
||||
const (
|
||||
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
|
||||
|
||||
fingerprintChrome = "chrome"
|
||||
fingerprintFirefox = "firefox"
|
||||
fingerprintSafari = "safari"
|
||||
fingerprintChrome Fingerprint = "chrome"
|
||||
fingerprintFirefox Fingerprint = "firefox"
|
||||
fingerprintSafari Fingerprint = "safari"
|
||||
)
|
||||
|
||||
func setFingerprint(client *req.Client, fingerprint string) {
|
||||
switch fingerprint {
|
||||
var currentFingerprint Fingerprint = ""
|
||||
|
||||
func setFingerprint(client *req.Client) {
|
||||
switch currentFingerprint {
|
||||
case fingerprintChrome:
|
||||
client.ImpersonateChrome()
|
||||
case fingerprintFirefox:
|
||||
|
||||
+6
-6
@@ -217,7 +217,7 @@
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.1",
|
||||
"@webassemblyjs/helper-wasm-section": "1.11.1",
|
||||
"@webassemblyjs/wasm-gen": "1.11.1",
|
||||
"@webassemblyjs/wasm-opts": "1.11.1",
|
||||
"@webassemblyjs/wasm-opt": "1.11.1",
|
||||
"@webassemblyjs/wasm-parser": "1.11.1",
|
||||
"@webassemblyjs/wast-printer": "1.11.1"
|
||||
}
|
||||
@@ -235,9 +235,9 @@
|
||||
"@webassemblyjs/utf8": "1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opts": {
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opts/-/wasm-opts-1.11.1.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
|
||||
"integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
@@ -2114,7 +2114,7 @@
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.1",
|
||||
"@webassemblyjs/helper-wasm-section": "1.11.1",
|
||||
"@webassemblyjs/wasm-gen": "1.11.1",
|
||||
"@webassemblyjs/wasm-opts": "1.11.1",
|
||||
"@webassemblyjs/wasm-opt": "1.11.1",
|
||||
"@webassemblyjs/wasm-parser": "1.11.1",
|
||||
"@webassemblyjs/wast-printer": "1.11.1"
|
||||
}
|
||||
@@ -2132,9 +2132,9 @@
|
||||
"@webassemblyjs/utf8": "1.11.1"
|
||||
}
|
||||
},
|
||||
"@webassemblyjs/wasm-opts": {
|
||||
"@webassemblyjs/wasm-opt": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opts/-/wasm-opts-1.11.1.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
|
||||
"integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
||||
@@ -22,16 +22,3 @@ func AssertError[T error](err error) (t T, r bool) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SafeGet[T any](vm *goja.Runtime, name string) T {
|
||||
v := vm.Get(name)
|
||||
if v == nil {
|
||||
var init T
|
||||
return init
|
||||
}
|
||||
if e, ok := v.Export().(T); ok {
|
||||
return e
|
||||
}
|
||||
var init T
|
||||
return init
|
||||
}
|
||||
Loaded 50 of 173 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user