mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-11 19:04:08 +00:00
Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76bec67ab2 | ||
|
|
a2a9e3dc2a | ||
|
|
9a5680fb45 | ||
|
|
75a8a3a9ac | ||
|
|
3e8c85b945 | ||
|
|
90f70d1bf6 | ||
|
|
20a70d7f91 | ||
|
|
d33546a4b5 | ||
|
|
93614d46a8 | ||
|
|
04b0e691eb | ||
|
|
7f08fa0b23 | ||
|
|
df6a51d23c | ||
|
|
46c379d556 | ||
|
|
46cd9f7a3e | ||
|
|
5c2f66f4b6 | ||
|
|
cfdcb586ee | ||
|
|
55e6bdc4f7 | ||
|
|
b31b42301e | ||
|
|
7ce0d876b0 | ||
|
|
2fd2596aa0 | ||
|
|
e0973e4a34 | ||
|
|
4f7b8a05ad | ||
|
|
2927d1ef2b | ||
|
|
3451f577ab | ||
|
|
06671be0d8 | ||
|
|
8ae9363f0f | ||
|
|
5d84445f8f | ||
|
|
7c541e4047 | ||
|
|
3262e5a11b | ||
|
|
ff43a373cd | ||
|
|
7394d948e4 | ||
|
|
5afb3aa43b | ||
|
|
7705af6e68 | ||
|
|
684e060ef3 | ||
|
|
0c67792441 | ||
|
|
b0ef2c2a87 | ||
|
|
1977c2a0fc | ||
|
|
d289708f9e | ||
|
|
cd5fd45667 | ||
|
|
50bc9ab129 |
No files matched your search
@@ -51,14 +51,22 @@ template: |
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td
|
<td><code>Flathub</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
+55
-39
@@ -13,7 +13,7 @@ on:
|
|||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
FLUTTER_VERSION: "3.24.5"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-2022
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -139,9 +139,9 @@ jobs:
|
|||||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
asset_path: ui/flutter/build/windows/Output/*
|
asset_path: ui/flutter/build/windows/Output/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-macos-arm64-lib:
|
build-macos-amd64-lib:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-13
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -157,15 +157,18 @@ jobs:
|
|||||||
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
|
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-amd64-lib
|
||||||
path: |
|
path: |
|
||||||
libgopeed.dylib
|
libgopeed.dylib
|
||||||
host
|
host
|
||||||
updater
|
updater
|
||||||
build-macos:
|
build-macos:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-13
|
runs-on: macos-latest
|
||||||
needs: [get-release, build-macos-arm64-lib]
|
strategy:
|
||||||
|
matrix:
|
||||||
|
channel: [ arm64, amd64, universal ]
|
||||||
|
needs: [get-release, build-macos-amd64-lib]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -183,34 +186,46 @@ jobs:
|
|||||||
npm install -g appdmg
|
npm install -g appdmg
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-amd64-lib
|
||||||
path: ui/flutter/lib-arm64
|
path: ui/flutter/lib-amd64
|
||||||
|
- if: ${{ matrix.channel == 'arm64' }}
|
||||||
|
name: Build Arm64 Libraries
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
- if: ${{ matrix.channel == 'amd64' }}
|
||||||
|
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
|
||||||
|
- if: ${{ matrix.channel == 'universal' }}
|
||||||
|
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
|
||||||
|
lipo -create -output ui/flutter/assets/exec/updater ui/flutter/.temp/updater.arm64 ui/flutter/.temp/updater.amd64
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
PROJECT_DIR=$(pwd)
|
cd ui/flutter
|
||||||
|
|
||||||
# amd64 lib
|
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
|
||||||
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/host github.com/GopeedLab/gopeed/cmd/host
|
|
||||||
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/updater github.com/GopeedLab/gopeed/cmd/updater
|
|
||||||
# universal binary
|
|
||||||
mkdir -p ui/flutter/macos/Frameworks
|
|
||||||
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
|
|
||||||
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
|
|
||||||
cp ui/flutter/lib-amd64/host ui/flutter/macos/Frameworks/amd64-host
|
|
||||||
cp ui/flutter/lib-arm64/host ui/flutter/macos/Frameworks/arm64-host
|
|
||||||
cp ui/flutter/lib-amd64/updater ui/flutter/macos/Frameworks/amd64-updater
|
|
||||||
cp ui/flutter/lib-arm64/updater ui/flutter/macos/Frameworks/arm64-updater
|
|
||||||
cd ui/flutter/macos/Frameworks
|
|
||||||
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
|
|
||||||
lipo -create -output host amd64-host arm64-host
|
|
||||||
lipo -create -output updater amd64-updater arm64-updater
|
|
||||||
rm -rf amd64-lib arm64-lib amd64-host arm64-host amd64-updater arm64-updater
|
|
||||||
mv host ../../assets/exec/host
|
|
||||||
|
|
||||||
cd $PROJECT_DIR/ui/flutter
|
|
||||||
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||||
cd build/macos/Build/Products/Release
|
cd build/macos/Build/Products/Release
|
||||||
cat>appdmg.json<<EOF
|
cat>appdmg.json<<EOF
|
||||||
@@ -225,6 +240,13 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
mkdir dist
|
mkdir dist
|
||||||
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
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
|
||||||
|
if [[ "${{ matrix.channel }}" == "amd64" ]]; then
|
||||||
|
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-amd64.dmg
|
||||||
|
fi
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -243,12 +265,7 @@ jobs:
|
|||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- if: matrix.os == 'ubuntu-22.04'
|
- uses: subosito/flutter-action@v2
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
|
||||||
- if: matrix.os == 'ubuntu-22.04-arm'
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: master
|
channel: master
|
||||||
@@ -260,7 +277,6 @@ jobs:
|
|||||||
sudo apt install -y libfuse2 locate
|
sudo apt install -y libfuse2 locate
|
||||||
arch=$(uname -m)
|
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"
|
||||||
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
|
||||||
chmod +x appimagetool
|
chmod +x appimagetool
|
||||||
sudo mv appimagetool /usr/local/bin/
|
sudo mv appimagetool /usr/local/bin/
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -486,7 +502,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
build-ios:
|
build-ios:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-14
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
FLUTTER_VERSION: "3.24.5"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -53,7 +53,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Run Unit tests.
|
- name: Run Unit tests.
|
||||||
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/) -v -covermode=count -coverprofile=coverage.txt
|
run: |
|
||||||
|
# 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
|
||||||
- uses: codecov/codecov-action@v4
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
@@ -63,8 +69,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
|
os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -96,8 +102,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest, ubuntu-latest ]
|
os: [macos-14, ubuntu-latest]
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -129,7 +135,7 @@ jobs:
|
|||||||
build-web:
|
build-web:
|
||||||
name: Build web check
|
name: Build web check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -149,7 +155,7 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
name: Build docker check
|
name: Build docker check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
|
|||||||
@@ -35,11 +35,19 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
|
|||||||
+10
-2
@@ -35,11 +35,19 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
|
|||||||
+10
-2
@@ -35,11 +35,19 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
|
|||||||
+10
-2
@@ -35,11 +35,19 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
|
|||||||
+10
-2
@@ -35,11 +35,19 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="6">🐧 Linux</td>
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//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"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dial() (net.Conn, error) {
|
||||||
|
return winio.DialPipe(`\\.\pipe\gopeed_host`, nil)
|
||||||
|
}
|
||||||
+69
-24
@@ -1,22 +1,24 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/browser"
|
"github.com/pkg/browser"
|
||||||
"github.com/shirou/gopsutil/v4/process"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const identifier = "gopeed"
|
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
|
Meta map[string]any `json:"meta"`
|
||||||
Params json.RawMessage `json:"params"`
|
Params json.RawMessage `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,31 +28,74 @@ type Response struct {
|
|||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
|
func check() (data bool, err error) {
|
||||||
"ping": func(params json.RawMessage) (data any, err error) {
|
conn, err := Dial()
|
||||||
processes, err := process.Processes()
|
if err != nil {
|
||||||
if err != nil {
|
return false, err
|
||||||
return false, err
|
}
|
||||||
}
|
defer conn.Close()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range processes {
|
func wakeup(hidden bool) error {
|
||||||
name, err := p.Name()
|
running, _ := check()
|
||||||
if err != nil {
|
if running {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
|
uri := "gopeed:"
|
||||||
return true, nil
|
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
|
||||||
}
|
}
|
||||||
return false, nil
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("start gopeed failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiMap = map[string]func(message *Message) (data any, err error){
|
||||||
|
"ping": func(message *Message) (data any, err error) {
|
||||||
|
return check()
|
||||||
},
|
},
|
||||||
"create": func(params json.RawMessage) (data any, err error) {
|
"create": func(message *Message) (data any, err error) {
|
||||||
var strParams string
|
buf, err := message.Params.MarshalJSON()
|
||||||
if err = json.Unmarshal(params, &strParams); err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
|
|
||||||
|
silent := false
|
||||||
|
if v, ok := message.Meta["silent"]; ok {
|
||||||
|
silent, _ = v.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wakeup(silent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return Dial()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", "http://127.0.0.1/create", bytes.NewBuffer(buf))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if message.Meta != nil {
|
||||||
|
metaJson, _ := json.Marshal(message.Meta)
|
||||||
|
req.Header.Set("X-Gopeed-Host-Meta", string(metaJson))
|
||||||
|
}
|
||||||
|
_, err = client.Do(req)
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -88,7 +133,7 @@ func main() {
|
|||||||
var data any
|
var data any
|
||||||
var err error
|
var err error
|
||||||
if handler, ok := apiMap[message.Method]; ok {
|
if handler, ok := apiMap[message.Method]; ok {
|
||||||
data, err = handler(message.Params)
|
data, err = handler(&message)
|
||||||
} else {
|
} else {
|
||||||
err = errors.New("Unknown method: " + message.Method)
|
err = errors.New("Unknown method: " + message.Method)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-6
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ func main() {
|
|||||||
pid := flag.Int("pid", 0, "PID of the process to update")
|
pid := flag.Int("pid", 0, "PID of the process to update")
|
||||||
updateChannel := flag.String("channel", "", "Update channel")
|
updateChannel := flag.String("channel", "", "Update channel")
|
||||||
packagePath := flag.String("asset", "", "Path to the package asset")
|
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")
|
logPath := flag.String("log", "", "Log file path")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ func main() {
|
|||||||
restart bool
|
restart bool
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
|
if restart, err = update(*pid, *updateChannel, *packagePath, *exeDir); err != nil {
|
||||||
log.Printf("Update failed: %v\n", err)
|
log.Printf("Update failed: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
|
func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, err error) {
|
||||||
killSignalChan := make(chan any, 1)
|
killSignalChan := make(chan any, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -76,9 +76,8 @@ func update(pid int, updateChannel, packagePath string) (restart bool, err error
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
appDir := filepath.Dir(os.Args[0])
|
log.Printf("Updating process updateChannel=%s packagePath=%s exeDir=%s\n", updateChannel, packagePath, exeDir)
|
||||||
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
|
if restart, err = install(killSignalChan, updateChannel, packagePath, exeDir); err != nil {
|
||||||
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
|
|
||||||
return false, errors.Wrap(err, "failed to install package")
|
return false, errors.Wrap(err, "failed to install package")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,16 +25,20 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
|
|||||||
mountPoint := ""
|
mountPoint := ""
|
||||||
for _, line := range strings.Split(string(output), "\n") {
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
if strings.Contains(line, "/Volumes/") {
|
if strings.Contains(line, "/Volumes/") {
|
||||||
fields := strings.Fields(line)
|
// Find the /Volumes/ path in the line
|
||||||
if len(fields) > 2 {
|
// hdiutil output format: /dev/disk4s1 Apple_HFS /Volumes/Gopeed
|
||||||
mountPoint = fields[len(fields)-1]
|
// 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:])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mountPoint == "" {
|
if mountPoint == "" {
|
||||||
return fmt.Errorf("failed to get mount point")
|
return fmt.Errorf("failed to get mount point from hdiutil output: %s", string(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detach the mounted DMG
|
// Detach the mounted DMG
|
||||||
@@ -45,7 +49,7 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return fmt.Errorf("no .app found in dmg")
|
return fmt.Errorf("no .app found in dmg, mountPoint: %s", mountPoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
killSignalChan <- nil
|
killSignalChan <- nil
|
||||||
|
|||||||
+166
-51
@@ -3,19 +3,23 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
Address *string `json:"address"`
|
Address *string `json:"address"`
|
||||||
Port *int `json:"port"`
|
Port *int `json:"port"`
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
ApiToken *string `json:"apiToken"`
|
ApiToken *string `json:"apiToken"`
|
||||||
StorageDir *string `json:"storageDir"`
|
StorageDir *string `json:"storageDir"`
|
||||||
|
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||||
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
@@ -23,62 +27,173 @@ type args struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parse() *args {
|
func parse() *args {
|
||||||
var cliArgs args
|
cfg := &args{}
|
||||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
|
||||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
cliConfig := loadCliArgs()
|
||||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
loadConfigFile(cfg, *cliConfig.configPath)
|
||||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
loadEnvVars(cfg)
|
||||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
|
// override with non-default command line arguments
|
||||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
overrideWithCliArgs(cfg, cliConfig)
|
||||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
// 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")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// args priority: config file > cli args
|
// Parse white download directories from comma-separated string
|
||||||
cfgArgs := loadConfig(*cliArgs.configPath)
|
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
|
||||||
if cfgArgs.Address == nil {
|
dirs := strings.Split(*whiteDownloadDirs, ",")
|
||||||
cfgArgs.Address = cliArgs.Address
|
for i := range dirs {
|
||||||
|
dirs[i] = strings.TrimSpace(dirs[i])
|
||||||
|
}
|
||||||
|
cfg.WhiteDownloadDirs = dirs
|
||||||
}
|
}
|
||||||
if cfgArgs.Port == nil {
|
|
||||||
cfgArgs.Port = cliArgs.Port
|
return cfg
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(path string) *args {
|
// overrideWithCliArgs overrides config with non-empty command line arguments
|
||||||
var args args
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if !filepath.IsAbs(path) {
|
// 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) {
|
||||||
dir, err := os.Getwd()
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
return
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
path = filepath.Join(dir, path)
|
configPath = filepath.Join(dir, configPath)
|
||||||
}
|
}
|
||||||
file, err := os.ReadFile(path)
|
|
||||||
|
file, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return &args
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("config file read failed, reason:" + err.Error())
|
return
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(file, &args); err != nil {
|
|
||||||
fmt.Println("config file parse failed, reason:" + err.Error())
|
if err = json.Unmarshal(file, cfg); err != nil {
|
||||||
return &args
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("config file loaded: %s\n", path)
|
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+19
-17
@@ -6,11 +6,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/cmd"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
@@ -23,36 +24,37 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := parse()
|
args := parse()
|
||||||
var webBasicAuth *model.WebBasicAuth
|
var webAuth *model.WebAuth
|
||||||
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
||||||
webBasicAuth = &model.WebBasicAuth{
|
webAuth = &model.WebAuth{
|
||||||
Username: *args.Username,
|
Username: *args.Username,
|
||||||
Password: *args.Password,
|
Password: *args.Password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dir string
|
var storageDir string
|
||||||
if args.StorageDir != nil && *args.StorageDir != "" {
|
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||||
dir = *args.StorageDir
|
storageDir = *args.StorageDir
|
||||||
} else {
|
} else {
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
dir = filepath.Dir(exe)
|
storageDir = filepath.Join(filepath.Dir(exe), "storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &model.StartConfig{
|
cfg := &model.StartConfig{
|
||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
StorageDir: filepath.Join(dir, "storage"),
|
StorageDir: storageDir,
|
||||||
ApiToken: *args.ApiToken,
|
WhiteDownloadDirs: args.WhiteDownloadDirs,
|
||||||
DownloadConfig: args.DownloadConfig,
|
ApiToken: *args.ApiToken,
|
||||||
ProductionMode: true,
|
DownloadConfig: args.DownloadConfig,
|
||||||
WebEnable: true,
|
ProductionMode: true,
|
||||||
WebFS: sub,
|
WebEnable: true,
|
||||||
WebBasicAuth: webBasicAuth,
|
WebFS: sub,
|
||||||
|
WebAuth: webAuth,
|
||||||
}
|
}
|
||||||
cmd.Start(cfg)
|
cmd.Start(cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ require (
|
|||||||
github.com/go-git/go-git/v5 v5.8.1
|
github.com/go-git/go-git/v5 v5.8.1
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/imroc/req/v3 v3.52.2
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
github.com/mattn/go-ieproxy v0.0.12
|
github.com/mattn/go-ieproxy v0.0.12
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@@ -21,17 +22,29 @@ require (
|
|||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.3.11
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/sync v0.9.0
|
golang.org/x/sync v0.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/ebitengine/purego v0.8.1 // indirect
|
github.com/ebitengine/purego v0.8.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/icholy/digest v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // 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/tklauser/go-sysconf v0.3.12 // indirect
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // 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
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -61,7 +74,7 @@ require (
|
|||||||
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -77,7 +90,7 @@ require (
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
@@ -122,13 +135,13 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
golang.org/x/crypto v0.29.0 // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/net v0.31.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.20.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.27.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
modernc.org/libc v1.61.0 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
|
|||||||
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
||||||
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
||||||
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
|
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
|
||||||
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
@@ -122,8 +124,9 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M
|
|||||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -196,6 +199,8 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
@@ -229,12 +234,12 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -250,6 +255,11 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
@@ -262,6 +272,10 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
|||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
|
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||||
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
|
github.com/imroc/req/v3 v3.52.2 h1:xJocr1aIv0a2K9knfBQ4JnZHk+kWTITdjf0mgDg229I=
|
||||||
|
github.com/imroc/req/v3 v3.52.2/go.mod h1:dBGsDloOSZJcFs6PnTjZXYBJK70OXbZpizHBLNqcH2k=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
@@ -277,6 +291,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
|||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
@@ -331,7 +347,11 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
|
|||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
@@ -380,6 +400,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
@@ -404,7 +426,13 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
|||||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
||||||
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
|
||||||
|
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||||
|
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
@@ -452,8 +480,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
||||||
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
@@ -471,6 +500,8 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
|
|||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -487,8 +518,12 @@ go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZk
|
|||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||||
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||||
|
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -497,8 +532,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||||
@@ -512,8 +547,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
|||||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -535,8 +570,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -550,8 +585,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -590,17 +625,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -609,8 +642,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -624,8 +657,8 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -646,6 +679,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
|||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -667,6 +702,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ type DefaultFileController struct {
|
|||||||
|
|
||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
GetConfig: func(v any) {},
|
GetConfig: func(v any) {},
|
||||||
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) {
|
||||||
|
return requestProxy.ToHandler()
|
||||||
|
},
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type config struct {
|
|||||||
ListenPort int `json:"listenPort"`
|
ListenPort int `json:"listenPort"`
|
||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
||||||
SeedKeep bool `json:"seedKeep`
|
SeedKeep bool `json:"seedKeep"`
|
||||||
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||||
SeedRatio float64 `json:"seedRatio"`
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
// SeedTime is the time in seconds to seed after downloading is complete.
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
"github.com/anacrolix/torrent"
|
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -19,6 +12,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -100,9 +101,6 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.meta.Req = req
|
f.meta.Req = req
|
||||||
if err := f.addTorrent(req, false); err != nil {
|
if err := f.addTorrent(req, false); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -355,10 +353,14 @@ func (f *Fetcher) WaitUpload() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||||
|
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = f.initClient(); err != nil {
|
if err = f.initClient(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
schema := util.ParseSchema(req.URL)
|
schema := util.ParseSchema(req.URL)
|
||||||
|
privateTorrent := false
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(req.URL)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else {
|
} else {
|
||||||
@@ -388,34 +390,46 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
|||||||
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info, er := metaInfo.UnmarshalInfo()
|
||||||
|
if er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Private != nil && *info.Private {
|
||||||
|
privateTorrent = true
|
||||||
|
}
|
||||||
f.torrent, err = client.AddTorrent(metaInfo)
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// use map to deduplicate
|
// Do not add external tracker to a private torrent.
|
||||||
trackers := make(map[string]bool)
|
if !privateTorrent {
|
||||||
if req.Extra != nil {
|
// use map to deduplicate
|
||||||
extra := req.Extra.(*bt.ReqExtra)
|
trackers := make(map[string]bool)
|
||||||
if len(extra.Trackers) > 0 {
|
if req.Extra != nil {
|
||||||
for _, tracker := range extra.Trackers {
|
extra := req.Extra.(*bt.ReqExtra)
|
||||||
|
if len(extra.Trackers) > 0 {
|
||||||
|
for _, tracker := range extra.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(f.config.Trackers) > 0 {
|
||||||
|
for _, tracker := range f.config.Trackers {
|
||||||
trackers[tracker] = true
|
trackers[tracker] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if len(trackers) > 0 {
|
||||||
if len(f.config.Trackers) > 0 {
|
announceList := make([][]string, 0)
|
||||||
for _, tracker := range f.config.Trackers {
|
for tracker := range trackers {
|
||||||
trackers[tracker] = true
|
announceList = append(announceList, []string{tracker})
|
||||||
|
}
|
||||||
|
f.torrent.AddTrackers(announceList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(trackers) > 0 {
|
|
||||||
announceList := make([][]string, 0)
|
|
||||||
for tracker := range trackers {
|
|
||||||
announceList = append(announceList, []string{tracker})
|
|
||||||
}
|
|
||||||
f.torrent.AddTrackers(announceList)
|
|
||||||
}
|
|
||||||
<-f.torrent.GotInfo()
|
<-f.torrent.GotInfo()
|
||||||
f.torrentReady.Store(true)
|
f.torrentReady.Store(true)
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ package bt
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"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"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"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) {
|
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ package bt
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/anacrolix/torrent/storage"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
|
||||||
"github.com/anacrolix/missinggo/v2"
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
|
||||||
"github.com/anacrolix/torrent/common"
|
"github.com/anacrolix/torrent/common"
|
||||||
@@ -34,7 +35,7 @@ func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
|
|||||||
if opts.FilePathMaker == nil {
|
if opts.FilePathMaker == nil {
|
||||||
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
if opts.Info.Name != metainfo.NoName {
|
if opts.Info.Length == 0 || opts.File.Path == nil {
|
||||||
parts = append(parts, opts.Info.Name)
|
parts = append(parts, opts.Info.Name)
|
||||||
}
|
}
|
||||||
return filepath.Join(append(parts, opts.File.Path...)...)
|
return filepath.Join(append(parts, opts.File.Path...)...)
|
||||||
|
|||||||
+239
-126
@@ -16,18 +16,23 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/xiaoqidun/setft"
|
"github.com/xiaoqidun/setft"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
const connectTimeout = 15 * time.Second
|
const (
|
||||||
const readTimeout = 15 * time.Second
|
connectTimeout = 15 * time.Second
|
||||||
|
readTimeout = 15 * time.Second
|
||||||
|
helpMinSize = 1 * 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
Code int
|
Code int
|
||||||
@@ -46,6 +51,12 @@ type chunk struct {
|
|||||||
Begin int64
|
Begin int64
|
||||||
End int64
|
End int64
|
||||||
Downloaded int64
|
Downloaded int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type connection struct {
|
||||||
|
Chunk *chunk
|
||||||
|
Downloaded int64
|
||||||
|
Completed bool
|
||||||
|
|
||||||
failed bool
|
failed bool
|
||||||
retryTimes int
|
retryTimes int
|
||||||
@@ -68,8 +79,11 @@ type Fetcher struct {
|
|||||||
config *config
|
config *config
|
||||||
doneCh chan error
|
doneCh chan error
|
||||||
|
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
chunks []*chunk
|
connections []*connection
|
||||||
|
helpLock sync.Mutex
|
||||||
|
redirectURL string
|
||||||
|
redirectLock sync.Mutex
|
||||||
|
|
||||||
file *os.File
|
file *os.File
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -159,7 +173,13 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
filename = strings.Replace(filename, "UTF8", "UTF-8", 1)
|
filename = strings.Replace(filename, "UTF8", "UTF-8", 1)
|
||||||
file.Name, _ = decoder.Decode(filename)
|
file.Name, _ = decoder.Decode(filename)
|
||||||
} else {
|
} else {
|
||||||
file.Name = filename
|
file.Name = util.TryUrlQueryUnescape(filename)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
substr := "attachment; filename="
|
||||||
|
index := strings.Index(contentDisposition, substr)
|
||||||
|
if index != -1 {
|
||||||
|
file.Name = util.TryUrlQueryUnescape(contentDisposition[index+len(substr):])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,9 +242,10 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.chunks == nil {
|
if f.connections == nil {
|
||||||
f.chunks = f.splitChunk()
|
f.connections = f.splitConnection()
|
||||||
}
|
}
|
||||||
|
f.redirectURL = ""
|
||||||
f.fetch()
|
f.fetch()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -251,16 +272,26 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Stats() any {
|
func (f *Fetcher) Stats() any {
|
||||||
// todo http download health
|
statsConnections := make([]*fhttp.StatsConnection, 0)
|
||||||
return &fhttp.Stats{}
|
for _, connection := range f.connections {
|
||||||
|
statsConnections = append(statsConnections, &fhttp.StatsConnection{
|
||||||
|
Downloaded: connection.Downloaded,
|
||||||
|
Completed: connection.Completed,
|
||||||
|
Failed: connection.failed,
|
||||||
|
RetryTimes: connection.retryTimes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &fhttp.Stats{
|
||||||
|
Connections: statsConnections,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
p := make(fetcher.Progress, 0)
|
p := make(fetcher.Progress, 0)
|
||||||
if len(f.chunks) > 0 {
|
if len(f.connections) > 0 {
|
||||||
total := int64(0)
|
total := int64(0)
|
||||||
for _, chunk := range f.chunks {
|
for _, connection := range f.connections {
|
||||||
total += chunk.Downloaded
|
total += connection.Downloaded
|
||||||
}
|
}
|
||||||
p = append(p, total)
|
p = append(p, total)
|
||||||
}
|
}
|
||||||
@@ -275,16 +306,16 @@ func (f *Fetcher) fetch() {
|
|||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
ctx, f.cancel = context.WithCancel(context.Background())
|
ctx, f.cancel = context.WithCancel(context.Background())
|
||||||
f.eg, _ = errgroup.WithContext(ctx)
|
f.eg, _ = errgroup.WithContext(ctx)
|
||||||
chunkErrs := make([]error, len(f.chunks))
|
connectionErrs := make([]error, len(f.connections))
|
||||||
for i := 0; i < len(f.chunks); i++ {
|
for i := 0; i < len(f.connections); i++ {
|
||||||
i := i
|
i := i
|
||||||
f.eg.Go(func() error {
|
f.eg.Go(func() error {
|
||||||
err := f.fetchChunk(i, ctx)
|
err := f.run(i, ctx)
|
||||||
// if canceled, fail fast
|
// if canceled, fail fast
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
chunkErrs[i] = err
|
connectionErrs[i] = err
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -296,7 +327,7 @@ func (f *Fetcher) fetch() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check all fetch results, if any error, return
|
// check all fetch results, if any error, return
|
||||||
for _, chunkErr := range chunkErrs {
|
for _, chunkErr := range connectionErrs {
|
||||||
if chunkErr != nil {
|
if chunkErr != nil {
|
||||||
err = chunkErr
|
err = chunkErr
|
||||||
break
|
break
|
||||||
@@ -312,122 +343,193 @@ func (f *Fetcher) fetch() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
func (f *Fetcher) run(index int, ctx context.Context) (err error) {
|
||||||
chunk := f.chunks[index]
|
connection := f.connections[index]
|
||||||
chunk.failed = false
|
connection.failed = false
|
||||||
chunk.retryTimes = 0
|
connection.retryTimes = 0
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client = f.buildClient()
|
client = f.buildClient()
|
||||||
buf = make([]byte, 8192)
|
buf = make([]byte, 8192)
|
||||||
)
|
)
|
||||||
// retry until all remain chunks failed
|
|
||||||
for {
|
downloadChunk := func(chunk *chunk) (err error) {
|
||||||
// if chunk is completed, return
|
// retry until all remain chunks failed
|
||||||
if f.meta.Res.Range && chunk.remain() <= 0 {
|
for {
|
||||||
return nil
|
// if chunk is completed, return
|
||||||
}
|
if f.meta.Res.Range && chunk.remain() <= 0 {
|
||||||
// if all chunks failed, return
|
return nil
|
||||||
if chunk.failed {
|
}
|
||||||
allFailed := true
|
// if all chunks failed, return
|
||||||
for _, c := range f.chunks {
|
if connection.failed {
|
||||||
if !c.failed {
|
allFailed := true
|
||||||
allFailed = false
|
for _, c := range f.connections {
|
||||||
break
|
if c.Completed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !c.failed {
|
||||||
|
allFailed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allFailed {
|
||||||
|
if connection.retryTimes >= 3 {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
connection.retryTimes++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allFailed {
|
|
||||||
if chunk.retryTimes >= 3 {
|
err = func() error {
|
||||||
return
|
var (
|
||||||
|
httpReq *http.Request
|
||||||
|
resp *http.Response
|
||||||
|
)
|
||||||
|
f.redirectLock.Lock()
|
||||||
|
redirectURL := f.redirectURL
|
||||||
|
if redirectURL != "" {
|
||||||
|
f.redirectLock.Unlock()
|
||||||
} else {
|
} else {
|
||||||
chunk.retryTimes++
|
// Only hold the lock when we need to potentially set redirectURL
|
||||||
}
|
defer func() {
|
||||||
}
|
if redirectURL == "" && err == nil {
|
||||||
}
|
f.redirectURL = resp.Request.URL.String()
|
||||||
|
|
||||||
var (
|
|
||||||
httpReq *http.Request
|
|
||||||
resp *http.Response
|
|
||||||
)
|
|
||||||
httpReq, err = f.buildRequest(ctx, f.meta.Req)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if f.meta.Res.Range {
|
|
||||||
httpReq.Header.Set(base.HttpHeaderRange,
|
|
||||||
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
|
||||||
} else {
|
|
||||||
chunk.Downloaded = 0
|
|
||||||
}
|
|
||||||
err = func() error {
|
|
||||||
resp, err = client.Do(httpReq)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
|
||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
chunk.failed = false
|
|
||||||
reader := NewTimeoutReader(resp.Body, readTimeout)
|
|
||||||
for {
|
|
||||||
n, err := reader.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
finished := false
|
|
||||||
if f.meta.Res.Range {
|
|
||||||
remain := chunk.remain()
|
|
||||||
// If downloaded bytes exceed the remain bytes, only write remain bytes
|
|
||||||
if remain < int64(n) {
|
|
||||||
n = int(remain)
|
|
||||||
finished = true
|
|
||||||
}
|
}
|
||||||
}
|
f.redirectLock.Unlock()
|
||||||
|
}()
|
||||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
chunk.Downloaded += int64(n)
|
|
||||||
|
|
||||||
if finished {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
err = func() (err error) {
|
||||||
return nil
|
httpReq, err = f.buildRequest(ctx, f.meta.Req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if f.meta.Res.Range {
|
||||||
|
httpReq.Header.Set(base.HttpHeaderRange,
|
||||||
|
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
||||||
|
} else {
|
||||||
|
chunk.Downloaded = 0
|
||||||
|
}
|
||||||
|
resp, err = client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||||
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connection.failed = false
|
||||||
|
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||||
|
for {
|
||||||
|
n, err := reader.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
finished := false
|
||||||
|
if f.meta.Res.Range {
|
||||||
|
remain := chunk.remain()
|
||||||
|
// If downloaded bytes exceed the remain bytes, only write remain bytes
|
||||||
|
if remain < int64(n) {
|
||||||
|
n = int(remain)
|
||||||
|
finished = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chunk.Downloaded += int64(n)
|
||||||
|
connection.Downloaded += int64(n)
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
// If canceled, do not retry
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// retry request after 1 second
|
||||||
|
connection.failed = true
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}()
|
break
|
||||||
if err != nil {
|
}
|
||||||
// If canceled, do not retry
|
return
|
||||||
if errors.Is(err, context.Canceled) {
|
}
|
||||||
return
|
|
||||||
}
|
for {
|
||||||
// retry request after 1 second
|
if err = downloadChunk(connection.Chunk); err != nil {
|
||||||
chunk.failed = true
|
return
|
||||||
time.Sleep(time.Second)
|
}
|
||||||
|
|
||||||
|
// check this connection is completed
|
||||||
|
if !f.meta.Res.Range || !f.helpOtherConnection(connection) {
|
||||||
|
connection.Completed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) helpOtherConnection(helper *connection) bool {
|
||||||
|
f.helpLock.Lock()
|
||||||
|
defer f.helpLock.Unlock()
|
||||||
|
|
||||||
|
// find the slowest connection
|
||||||
|
var maxRemainConnection *connection
|
||||||
|
var maxRemain int64
|
||||||
|
for _, r := range f.connections {
|
||||||
|
if r == helper || r.Completed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
remain := r.Chunk.remain()
|
||||||
|
if remain > maxRemain && remain > helpMinSize {
|
||||||
|
maxRemainConnection = r
|
||||||
|
maxRemain = remain
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
if maxRemainConnection == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-calculate the chunk range
|
||||||
|
helper.Chunk.Begin = maxRemainConnection.Chunk.End - maxRemainConnection.Chunk.remain()/2
|
||||||
|
helper.Chunk.End = maxRemainConnection.Chunk.End
|
||||||
|
helper.Chunk.Downloaded = 0
|
||||||
|
maxRemainConnection.Chunk.End = helper.Chunk.Begin - 1
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
|
||||||
url, err := url.Parse(req.URL)
|
var reqUrl string
|
||||||
if err != nil {
|
if f.redirectURL != "" {
|
||||||
return
|
reqUrl = f.redirectURL
|
||||||
|
} else {
|
||||||
|
reqUrl = req.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
method string
|
method string
|
||||||
body io.Reader
|
body io.Reader
|
||||||
)
|
)
|
||||||
|
|
||||||
headers := http.Header{}
|
headers := http.Header{}
|
||||||
if req.Extra == nil {
|
if req.Extra == nil {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
@@ -440,7 +542,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
if len(extra.Header) > 0 {
|
if len(extra.Header) > 0 {
|
||||||
for k, v := range extra.Header {
|
for k, v := range extra.Header {
|
||||||
headers.Set(k, v)
|
headers.Set(k, strings.TrimSpace(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if extra.Body != "" {
|
if extra.Body != "" {
|
||||||
@@ -448,13 +550,13 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
|
||||||
headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
|
headers.Set(base.HttpHeaderUserAgent, strings.TrimSpace(f.config.UserAgent))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
|
httpReq, err = http.NewRequestWithContext(ctx, method, reqUrl, body)
|
||||||
} else {
|
} else {
|
||||||
httpReq, err = http.NewRequest(method, url.String(), body)
|
httpReq, err = http.NewRequest(method, reqUrl, body)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -467,30 +569,33 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
return httpReq, nil
|
return httpReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
func (f *Fetcher) splitConnection() (connections []*connection) {
|
||||||
if f.meta.Res.Range {
|
if f.meta.Res.Range {
|
||||||
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
optConnections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
||||||
// 每个连接平均需要下载的分块大小
|
// 每个连接平均需要下载的分块大小
|
||||||
chunkSize := f.meta.Res.Size / int64(connections)
|
chunkSize := f.meta.Res.Size / int64(optConnections)
|
||||||
chunks = make([]*chunk, connections)
|
connections = make([]*connection, optConnections)
|
||||||
for i := 0; i < connections; i++ {
|
for i := 0; i < optConnections; i++ {
|
||||||
var (
|
var (
|
||||||
begin = chunkSize * int64(i)
|
begin = chunkSize * int64(i)
|
||||||
end int64
|
end int64
|
||||||
)
|
)
|
||||||
if i == connections-1 {
|
if i == optConnections-1 {
|
||||||
// 最后一个分块需要保证把文件下载完
|
// 最后一个分块需要保证把文件下载完
|
||||||
end = f.meta.Res.Size - 1
|
end = f.meta.Res.Size - 1
|
||||||
} else {
|
} else {
|
||||||
end = begin + chunkSize - 1
|
end = begin + chunkSize - 1
|
||||||
}
|
}
|
||||||
chunk := newChunk(begin, end)
|
connections[i] = &connection{
|
||||||
chunks[i] = chunk
|
Chunk: newChunk(begin, end),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 只支持单连接下载
|
// 只支持单连接下载
|
||||||
chunks = make([]*chunk, 1)
|
connections = make([]*connection, 1)
|
||||||
chunks[0] = newChunk(0, 0)
|
connections[0] = &connection{
|
||||||
|
Chunk: newChunk(0, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -513,8 +618,16 @@ func (f *Fetcher) buildClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeMangledString(mangled string) string {
|
||||||
|
rawBytes := make([]byte, 0, len(mangled)*3)
|
||||||
|
for _, r := range mangled {
|
||||||
|
rawBytes = append(rawBytes, byte(r))
|
||||||
|
}
|
||||||
|
return string(rawBytes)
|
||||||
|
}
|
||||||
|
|
||||||
type fetcherData struct {
|
type fetcherData struct {
|
||||||
Chunks []*chunk
|
Connections []*connection
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherManager struct {
|
type FetcherManager struct {
|
||||||
@@ -570,7 +683,7 @@ func (fm *FetcherManager) DefaultConfig() any {
|
|||||||
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
_f := f.(*Fetcher)
|
_f := f.(*Fetcher)
|
||||||
return &fetcherData{
|
return &fetcherData{
|
||||||
Chunks: _f.chunks,
|
Connections: _f.connections,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,8 +695,8 @@ func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
fetcher.meta = meta
|
fetcher.meta = meta
|
||||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||||
if len(fd.Chunks) > 0 {
|
if len(fd.Connections) > 0 {
|
||||||
fetcher.chunks = fd.Chunks
|
fetcher.connections = fd.Connections
|
||||||
}
|
}
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, "no-encode", &base.Resource{
|
||||||
|
Size: test.BuildSize,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "测试.zip",
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
|
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Range: false,
|
Range: false,
|
||||||
@@ -95,6 +105,21 @@ func TestFetcher_ResolveWithHostHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveWithInvalidHeader(t *testing.T) {
|
||||||
|
fetcher := buildFetcher()
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "https://bing.com",
|
||||||
|
Extra: &http.ReqExtra{
|
||||||
|
Header: map[string]string{
|
||||||
|
"Referer": "\rtest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Resolve() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
||||||
listener := startTestServer()
|
listener := startTestServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -246,6 +271,33 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Stats(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
}), listener, 0, t)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fetcher.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stats := fetcher.Stats().(*http.Stats)
|
||||||
|
if len(stats.Connections) != 16 {
|
||||||
|
t.Errorf("Stats() got = %v, want %v", len(stats.Connections), 16)
|
||||||
|
}
|
||||||
|
totalDownloaded := int64(0)
|
||||||
|
for _, conn := range stats.Connections {
|
||||||
|
totalDownloaded += conn.Downloaded
|
||||||
|
}
|
||||||
|
if totalDownloaded != test.BuildSize {
|
||||||
|
t.Errorf("Stats() got = %v, want %v", totalDownloaded, test.BuildSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcherManager_ParseName(t *testing.T) {
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
u string
|
u string
|
||||||
|
|||||||
@@ -102,6 +102,17 @@ func StartTestCustomServer() net.Listener {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
io.Copy(writer, file)
|
io.Copy(writer, file)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/no-encode", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Disposition", "attachment; filename=测试.zip")
|
||||||
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,16 @@ func ParseOptsExtra[E any](opts *Options) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateTaskBatch struct {
|
||||||
|
Reqs []*CreateTaskBatchItem `json:"reqs"`
|
||||||
|
Opts *Options `json:"opts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskBatchItem struct {
|
||||||
|
Req *Request `json:"req"`
|
||||||
|
Opts *Options `json:"opts"`
|
||||||
|
}
|
||||||
|
|
||||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||||
type DownloaderStoreConfig struct {
|
type DownloaderStoreConfig struct {
|
||||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||||
|
|||||||
+38
-30
@@ -2,15 +2,6 @@ package download
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/logger"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/pkgerrors"
|
|
||||||
"math"
|
"math"
|
||||||
gohttp "net/http"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -22,6 +13,16 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/logger"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -148,6 +149,7 @@ func (d *Downloader) Setup() error {
|
|||||||
tasks = append(tasks[:i], tasks[i+1:]...)
|
tasks = append(tasks[:i], tasks[i+1:]...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
d.assignFetcherManager(task)
|
||||||
initTask(task)
|
initTask(task)
|
||||||
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
|
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
|
||||||
task.Status = base.DownloadStatusPause
|
task.Status = base.DownloadStatusPause
|
||||||
@@ -358,10 +360,14 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
|
|||||||
return d.doCreate(fetcher, opts)
|
return d.doCreate(fetcher, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
|
func (d *Downloader) CreateDirectBatch(req *base.CreateTaskBatch) (taskId []string, err error) {
|
||||||
taskIds := make([]string, 0)
|
taskIds := make([]string, 0)
|
||||||
for _, req := range reqs {
|
for _, ir := range req.Reqs {
|
||||||
taskId, err := d.CreateDirect(req, opts.Clone())
|
opts := ir.Opts
|
||||||
|
if opts == nil {
|
||||||
|
opts = req.Opts
|
||||||
|
}
|
||||||
|
taskId, err := d.CreateDirect(ir.Req, opts.Clone())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -527,7 +533,7 @@ func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
|
|||||||
|
|
||||||
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||||
if filter == nil || filter.IsEmpty() {
|
if filter == nil || filter.IsEmpty() {
|
||||||
return d.deleteAll()
|
return d.deleteAll(force)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTasks := d.GetTasksByFilter(filter)
|
deleteTasks := d.GetTasksByFilter(filter)
|
||||||
@@ -572,17 +578,21 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) deleteAll() (err error) {
|
func (d *Downloader) deleteAll(force bool) (err error) {
|
||||||
|
var deleteTasksTemp []*Task
|
||||||
func() {
|
func() {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
deleteTasksTemp = append(deleteTasksTemp, task)
|
||||||
|
}
|
||||||
d.tasks = make([]*Task, 0)
|
d.tasks = make([]*Task, 0)
|
||||||
d.waitTasks = make([]*Task, 0)
|
d.waitTasks = make([]*Task, 0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, task := range d.tasks {
|
for _, task := range deleteTasksTemp {
|
||||||
if err = d.doDelete(task, true); err != nil {
|
if err = d.doDelete(task, force); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -892,18 +902,7 @@ func (d *Downloader) restoreTask(task *Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) restoreFetcher(task *Task) error {
|
func (d *Downloader) restoreFetcher(task *Task) error {
|
||||||
var fm fetcher.FetcherManager
|
v, f := task.fetcherManager.Restore()
|
||||||
for _, f := range d.cfg.FetchManagers {
|
|
||||||
if f.Name() == task.Protocol {
|
|
||||||
fm = f
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fm == nil {
|
|
||||||
return ErrUnSupportedProtocol
|
|
||||||
}
|
|
||||||
task.fetcherManager = fm
|
|
||||||
v, f := fm.Restore()
|
|
||||||
if v != nil {
|
if v != nil {
|
||||||
err := d.storage.Pop(bucketSave, task.ID, v)
|
err := d.storage.Pop(bucketSave, task.ID, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -943,9 +942,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if enable white download directory, check if the download directory is in the white list
|
// if enable white download directory, check if the download directory is in the white list
|
||||||
if len(d.cfg.DownloadDirWhiteList) > 0 {
|
if len(d.cfg.WhiteDownloadDirs) > 0 {
|
||||||
inWhiteList := false
|
inWhiteList := false
|
||||||
for _, dir := range d.cfg.DownloadDirWhiteList {
|
for _, dir := range d.cfg.WhiteDownloadDirs {
|
||||||
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
||||||
inWhiteList = true
|
inWhiteList = true
|
||||||
break
|
break
|
||||||
@@ -1145,6 +1144,15 @@ func logPanic(logDir string) {
|
|||||||
debug.SetCrashOutput(f, debug.CrashOptions{})
|
debug.SetCrashOutput(f, debug.CrashOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) assignFetcherManager(task *Task) error {
|
||||||
|
fm, err := d.parseFm(task.Meta.Req.URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.fetcherManager = fm
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||||
fm, err := d.parseFm(url)
|
fm, err := d.parseFm(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
downloader := NewDownloader(&DownloaderConfig{
|
downloader := NewDownloader(&DownloaderConfig{
|
||||||
DownloadDirWhiteList: []string{"./downloads"},
|
WhiteDownloadDirs: []string{"./downloads"},
|
||||||
})
|
})
|
||||||
if err := downloader.Setup(); err != nil {
|
if err := downloader.Setup(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -137,13 +137,15 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
fileNames := make([]string, 0)
|
fileNames := make([]string, 0)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
}
|
}
|
||||||
reqs = append(reqs, req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: req,
|
||||||
|
})
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fileNames = append(fileNames, test.DownloadName)
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
} else {
|
} else {
|
||||||
@@ -160,11 +162,14 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
_, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
|
||||||
Path: test.Dir,
|
Reqs: reqs,
|
||||||
Name: test.DownloadName,
|
Opts: &base.Options{
|
||||||
Extra: http.OptsExtra{
|
Path: test.Dir,
|
||||||
Connections: 4,
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -499,13 +504,15 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
|||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
fileNames := make([]string, 0)
|
fileNames := make([]string, 0)
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
}
|
}
|
||||||
reqs = append(reqs, req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: req,
|
||||||
|
})
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fileNames = append(fileNames, test.DownloadName)
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
} else {
|
} else {
|
||||||
@@ -522,11 +529,14 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
taskIds, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
|
||||||
Path: test.Dir,
|
Reqs: reqs,
|
||||||
Name: test.DownloadName,
|
Opts: &base.Options{
|
||||||
Extra: http.OptsExtra{
|
Path: test.Dir,
|
||||||
Connections: 4,
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -205,6 +205,31 @@ function testTimeout(){
|
|||||||
xhr.send();
|
xhr.send();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testFingerprint(fingerprint,ua){
|
||||||
|
__gopeed_setFingerprint(fingerprint);
|
||||||
|
const resp = await fetch(host+'/ua');
|
||||||
|
const data = await resp.json();
|
||||||
|
if(!data.user_agent.includes(ua)){
|
||||||
|
throw new Error('fingerprint test failed, user agent: ' + data.user_agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintDefault(){
|
||||||
|
await testFingerprint('none', 'Go')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintChrome(){
|
||||||
|
await testFingerprint('chrome', 'Chrome')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintFirefox(){
|
||||||
|
await testFingerprint('firefox', 'Firefox')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintSafari(){
|
||||||
|
await testFingerprint('safari', 'Safari')
|
||||||
|
}
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -288,6 +313,19 @@ function testTimeout(){
|
|||||||
if err == nil || err.Error() != "timeout" {
|
if err == nil || err.Error() != "timeout" {
|
||||||
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
|
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = callTestFun(engine, "testFingerprintChrome")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintChrome test failed", err)
|
||||||
|
}
|
||||||
|
_, err = callTestFun(engine, "testFingerprintFirefox")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintFirefox test failed", err)
|
||||||
|
}
|
||||||
|
_, err = callTestFun(engine, "testFingerprintSafari")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintSafari test failed", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchWithProxy(t *testing.T) {
|
func TestFetchWithProxy(t *testing.T) {
|
||||||
@@ -470,6 +508,15 @@ func startServer() net.Listener {
|
|||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/ua", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
data := map[string]any{
|
||||||
|
"user_agent": r.UserAgent(),
|
||||||
|
}
|
||||||
|
buf, _ := json.Marshal(data)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(buf)
|
||||||
|
})
|
||||||
server.Handler = mux
|
server.Handler = mux
|
||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
return listener
|
return listener
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -129,7 +130,8 @@ type XMLHttpRequest struct {
|
|||||||
requestHeaders http.Header
|
requestHeaders http.Header
|
||||||
responseHeaders http.Header
|
responseHeaders http.Header
|
||||||
aborted bool
|
aborted bool
|
||||||
proxyHandler func(r *http.Request) (*url.URL, error)
|
client *req.Client
|
||||||
|
fingerprint string
|
||||||
|
|
||||||
WithCredentials bool `json:"withCredentials"`
|
WithCredentials bool `json:"withCredentials"`
|
||||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||||
@@ -163,25 +165,39 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||||
var req *http.Request
|
setFingerprint(xhr.client, xhr.fingerprint)
|
||||||
var err error
|
|
||||||
d := xhr.parseData(data)
|
d := xhr.parseData(data)
|
||||||
var (
|
var (
|
||||||
contentType string
|
contentType string
|
||||||
contentLength int64
|
contentLength int64
|
||||||
isStringBody bool
|
isStringBody bool
|
||||||
)
|
)
|
||||||
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
// Create request using req library
|
||||||
} else {
|
reqBuilder := xhr.client.R()
|
||||||
|
|
||||||
|
// Set headers first
|
||||||
|
if xhr.requestHeaders != nil {
|
||||||
|
for key, values := range xhr.requestHeaders {
|
||||||
|
if len(values) > 0 {
|
||||||
|
// Merge multiple values with comma separator (HTTP standard)
|
||||||
|
mergedValue := strings.Join(values, ", ")
|
||||||
|
reqBuilder.SetHeader(key, mergedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle request body
|
||||||
|
if d != nil && xhr.method != "GET" && xhr.method != "HEAD" {
|
||||||
switch v := d.(type) {
|
switch v := d.(type) {
|
||||||
case string:
|
case string:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
|
reqBuilder.SetBody(v)
|
||||||
contentType = "text/plain;charset=UTF-8"
|
contentType = "text/plain;charset=UTF-8"
|
||||||
contentLength = int64(len(v))
|
contentLength = int64(len(v))
|
||||||
isStringBody = true
|
isStringBody = true
|
||||||
case *file.File:
|
case *file.File:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
|
reqBuilder.SetBody(v.Reader)
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
contentLength = v.Size
|
contentLength = v.Size
|
||||||
case *formdata.FormData:
|
case *formdata.FormData:
|
||||||
@@ -203,43 +219,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
defer mw.Close()
|
defer mw.Close()
|
||||||
mw.Send()
|
mw.Send()
|
||||||
}()
|
}()
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, pr)
|
reqBuilder.SetBody(pr)
|
||||||
contentType = mw.FormDataContentType()
|
contentType = mw.FormDataContentType()
|
||||||
contentLength = mw.Size()
|
contentLength = mw.Size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
xhr.callOnerror()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Header = xhr.requestHeaders
|
|
||||||
// Only string body can specify Content-Type header by user
|
// Only string body can specify Content-Type header by user
|
||||||
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
|
if contentType != "" && (!isStringBody || xhr.requestHeaders.Get("Content-Type") == "") {
|
||||||
req.Header.Set("Content-Type", contentType)
|
reqBuilder.SetHeader("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
if contentLength > 0 {
|
|
||||||
req.ContentLength = contentLength
|
// Set timeout
|
||||||
|
if xhr.Timeout > 0 {
|
||||||
|
xhr.client.SetTimeout(time.Duration(xhr.Timeout) * time.Millisecond)
|
||||||
}
|
}
|
||||||
transport := &http.Transport{
|
|
||||||
Proxy: xhr.proxyHandler,
|
// Configure redirect behavior
|
||||||
}
|
xhr.client.SetRedirectPolicy(func(req *http.Request, via []*http.Request) error {
|
||||||
client := &http.Client{
|
if xhr.Redirect == redirectManual {
|
||||||
Transport: transport,
|
return http.ErrUseLastResponse
|
||||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
}
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
if xhr.Redirect == redirectError {
|
||||||
if xhr.Redirect == redirectManual {
|
return errors.New("redirect failed")
|
||||||
return http.ErrUseLastResponse
|
}
|
||||||
}
|
if len(via) > 20 {
|
||||||
if xhr.Redirect == redirectError {
|
return errors.New("too many redirects")
|
||||||
return errors.New("redirect failed")
|
}
|
||||||
}
|
return nil
|
||||||
if len(via) > 20 {
|
})
|
||||||
return errors.New("too many redirects")
|
|
||||||
}
|
// Execute request
|
||||||
return nil
|
resp, err := reqBuilder.Send(xhr.method, xhr.url)
|
||||||
},
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// handle timeout error
|
// handle timeout error
|
||||||
var ne net.Error
|
var ne net.Error
|
||||||
@@ -254,35 +265,37 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
xhr.callOnerror()
|
xhr.callOnerror()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
xhr.Upload.callOnprogress(contentLength, contentLength)
|
xhr.Upload.callOnprogress(contentLength, contentLength)
|
||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.Upload.callOnload()
|
xhr.Upload.callOnload()
|
||||||
}
|
}
|
||||||
|
|
||||||
responseUrl := resp.Request.URL
|
// Set response URL (final URL after redirects)
|
||||||
responseUrl.Fragment = ""
|
if resp.Response != nil && resp.Response.Request != nil && resp.Response.Request.URL != nil {
|
||||||
xhr.ResponseUrl = responseUrl.String()
|
responseUrl := resp.Response.Request.URL
|
||||||
|
responseUrl.Fragment = ""
|
||||||
|
xhr.ResponseUrl = responseUrl.String()
|
||||||
|
} else {
|
||||||
|
xhr.ResponseUrl = xhr.url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set response headers
|
||||||
xhr.responseHeaders = resp.Header
|
xhr.responseHeaders = resp.Header
|
||||||
xhr.Status = resp.StatusCode
|
xhr.Status = resp.StatusCode
|
||||||
xhr.StatusText = resp.Status
|
xhr.StatusText = resp.Status
|
||||||
xhr.doReadystatechange(2)
|
xhr.doReadystatechange(2)
|
||||||
buf, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
bodyBytes := resp.Bytes()
|
||||||
xhr.callOnerror()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xhr.doReadystatechange(3)
|
xhr.doReadystatechange(3)
|
||||||
xhr.Response = string(buf)
|
xhr.Response = string(bodyBytes)
|
||||||
xhr.ResponseText = xhr.Response
|
xhr.ResponseText = xhr.Response
|
||||||
xhr.doReadystatechange(4)
|
xhr.doReadystatechange(4)
|
||||||
respBodyLen := int64(len(buf))
|
respBodyLen := int64(len(bodyBytes))
|
||||||
xhr.callOnprogress(respBodyLen, respBodyLen)
|
xhr.callOnprogress(respBodyLen, respBodyLen)
|
||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.callOnload()
|
xhr.callOnload()
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) Abort() {
|
func (xhr *XMLHttpRequest) Abort() {
|
||||||
@@ -293,6 +306,9 @@ func (xhr *XMLHttpRequest) Abort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
||||||
|
if xhr.responseHeaders == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +375,15 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
|||||||
return instanceValue
|
return instanceValue
|
||||||
})
|
})
|
||||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
// Create req client with proxy support
|
||||||
|
client := req.C()
|
||||||
|
if proxyHandler != nil {
|
||||||
|
client.SetProxy(proxyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
instance := &XMLHttpRequest{
|
instance := &XMLHttpRequest{
|
||||||
proxyHandler: proxyHandler,
|
client: client,
|
||||||
|
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
|
||||||
Upload: &XMLHttpRequestUpload{
|
Upload: &XMLHttpRequestUpload{
|
||||||
EventProp: &EventProp{
|
EventProp: &EventProp{
|
||||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||||
@@ -374,6 +397,9 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
|||||||
instanceValue.SetPrototype(call.This.Prototype())
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
return instanceValue
|
return instanceValue
|
||||||
})
|
})
|
||||||
|
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
|
||||||
|
runtime.Set(FingerprintMagicKey, fingerprint)
|
||||||
|
})
|
||||||
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package xhr
|
||||||
|
|
||||||
|
import "github.com/imroc/req/v3"
|
||||||
|
|
||||||
|
type Fingerprint string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
|
||||||
|
|
||||||
|
fingerprintChrome = "chrome"
|
||||||
|
fingerprintFirefox = "firefox"
|
||||||
|
fingerprintSafari = "safari"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setFingerprint(client *req.Client, fingerprint string) {
|
||||||
|
switch fingerprint {
|
||||||
|
case fingerprintChrome:
|
||||||
|
client.ImpersonateChrome()
|
||||||
|
case fingerprintFirefox:
|
||||||
|
client.ImpersonateFirefox()
|
||||||
|
case fingerprintSafari:
|
||||||
|
client.ImpersonateSafari()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,3 +22,16 @@ func AssertError[T error](err error) (t T, r bool) {
|
|||||||
}
|
}
|
||||||
return
|
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
|
||||||
|
}
|
||||||
@@ -137,10 +137,10 @@ type DownloaderConfig struct {
|
|||||||
Controller *controller.Controller
|
Controller *controller.Controller
|
||||||
FetchManagers []fetcher.FetcherManager
|
FetchManagers []fetcher.FetcherManager
|
||||||
|
|
||||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
|
||||||
Storage Storage
|
Storage Storage
|
||||||
StorageDir string
|
StorageDir string
|
||||||
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
WhiteDownloadDirs []string
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ type OptsExtra struct {
|
|||||||
|
|
||||||
// Stats for download
|
// Stats for download
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
// http stats
|
Connections []*StatsConnection `json:"connections"`
|
||||||
// health indicators of http
|
}
|
||||||
|
|
||||||
|
type StatsConnection struct {
|
||||||
|
Downloaded int64 `json:"downloaded"`
|
||||||
|
Completed bool `json:"completed"`
|
||||||
|
Failed bool `json:"failed"`
|
||||||
|
RetryTimes int `json:"retryTimes"`
|
||||||
}
|
}
|
||||||
+4
-2
@@ -58,13 +58,13 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||||
var req model.CreateTaskBatch
|
var req base.CreateTaskBatch
|
||||||
if ReadJson(r, w, &req) {
|
if ReadJson(r, w, &req) {
|
||||||
if len(req.Reqs) == 0 {
|
if len(req.Reqs) == 0 {
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
taskIds, err := Downloader.CreateDirectBatch(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
@@ -309,6 +309,8 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
r.RequestURI = ""
|
r.RequestURI = ""
|
||||||
r.URL = targetUrl
|
r.URL = targetUrl
|
||||||
r.Host = targetUrl.Host
|
r.Host = targetUrl.Host
|
||||||
|
r.Header.Del("Authorization")
|
||||||
|
r.Header.Del("X-Target-Uri")
|
||||||
resp, err := http.DefaultClient.Do(r)
|
resp, err := http.DefaultClient.Do(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, err.Error())
|
writeError(w, err.Error())
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
@@ -25,9 +24,9 @@ type StartConfig struct {
|
|||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
WebEnable bool
|
WebEnable bool
|
||||||
WebFS fs.FS
|
WebFS fs.FS
|
||||||
WebBasicAuth *WebBasicAuth
|
WebAuth *WebAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *StartConfig) Init() *StartConfig {
|
func (cfg *StartConfig) Init() *StartConfig {
|
||||||
@@ -49,13 +48,7 @@ func (cfg *StartConfig) Init() *StartConfig {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebBasicAuth struct {
|
type WebAuth struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization returns the value of the Authorization header to be used in HTTP requests.
|
|
||||||
func (cfg *WebBasicAuth) Authorization() string {
|
|
||||||
userId := cfg.Username + ":" + cfg.Password
|
|
||||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,3 @@ type CreateTask struct {
|
|||||||
Req *base.Request `json:"req"`
|
Req *base.Request `json:"req"`
|
||||||
Opt *base.Options `json:"opt"`
|
Opt *base.Options `json:"opt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateTaskBatch struct {
|
|
||||||
Reqs []*base.Request `json:"reqs"`
|
|
||||||
Opt *base.Options `json:"opt"`
|
|
||||||
}
|
|
||||||
+129
-12
@@ -2,6 +2,10 @@ package rest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
@@ -10,12 +14,14 @@ import (
|
|||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -23,6 +29,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
srv *http.Server
|
srv *http.Server
|
||||||
runningPort int
|
runningPort int
|
||||||
|
aesKey []byte
|
||||||
|
|
||||||
Downloader *download.Downloader
|
Downloader *download.Downloader
|
||||||
)
|
)
|
||||||
@@ -58,7 +65,9 @@ func Stop() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if srv != nil {
|
if srv != nil {
|
||||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +85,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
startCfg.Init()
|
startCfg.Init()
|
||||||
|
|
||||||
downloadCfg := &download.DownloaderConfig{
|
downloadCfg := &download.DownloaderConfig{
|
||||||
ProductionMode: startCfg.ProductionMode,
|
ProductionMode: startCfg.ProductionMode,
|
||||||
RefreshInterval: startCfg.RefreshInterval,
|
RefreshInterval: startCfg.RefreshInterval,
|
||||||
|
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
|
||||||
}
|
}
|
||||||
if startCfg.Storage == model.StorageBolt {
|
if startCfg.Storage == model.StorageBolt {
|
||||||
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
||||||
@@ -95,6 +105,13 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
util.SafeRemove(startCfg.Address)
|
util.SafeRemove(startCfg.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if startCfg.WebEnable {
|
||||||
|
aesKey = make([]byte, 32)
|
||||||
|
if _, err := rand.Read(aesKey); err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "generate aes key failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -125,15 +142,40 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
||||||
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
||||||
|
|
||||||
|
enableApiToken := startCfg.ApiToken != ""
|
||||||
|
enableBasicAuth := startCfg.WebEnable && startCfg.WebAuth != nil
|
||||||
if startCfg.WebEnable {
|
if startCfg.WebEnable {
|
||||||
|
if enableBasicAuth {
|
||||||
|
r.Methods(http.MethodPost).Path("/api/web/login").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var loginReq model.WebAuth
|
||||||
|
if ReadJson(r, w, &loginReq) {
|
||||||
|
if loginReq.Username == startCfg.WebAuth.Username && loginReq.Password == startCfg.WebAuth.Password {
|
||||||
|
// Generate a login token, Username:Password:Timestamp
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
tokenData := fmt.Sprintf("%s:%s:%d", loginReq.Username, loginReq.Password, timestamp)
|
||||||
|
token, err := aesEncrypt(aesKey, []byte(tokenData))
|
||||||
|
if err != nil {
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJson(w, model.NewOkResult(token))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
||||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||||
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
|
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
enableApiToken := startCfg.ApiToken != ""
|
|
||||||
enableBasicAuth := startCfg.WebEnable && startCfg.WebBasicAuth != nil
|
|
||||||
if enableApiToken || enableBasicAuth {
|
if enableApiToken || enableBasicAuth {
|
||||||
|
writeUnauthorized := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||||
|
}
|
||||||
|
|
||||||
r.Use(func(h http.Handler) http.Handler {
|
r.Use(func(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if enableApiToken {
|
if enableApiToken {
|
||||||
@@ -145,19 +187,49 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
writeUnauthorized(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
if enableBasicAuth {
|
||||||
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
if !strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api/web/login" {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
|
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token = strings.TrimPrefix(token, "Bearer ")
|
||||||
|
tokenData, err := aesDecrypt(aesKey, token)
|
||||||
|
if err != nil {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(string(tokenData), ":", 3)
|
||||||
|
username := parts[0]
|
||||||
|
password := parts[1]
|
||||||
|
timestamp, _ := strconv.Atoi(parts[2])
|
||||||
|
|
||||||
|
if username != startCfg.WebAuth.Username || password != startCfg.WebAuth.Password {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the token is expired (7 days)
|
||||||
|
if time.Now().Unix()-int64(timestamp) > 7*24*3600 {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
writeUnauthorized(w, r)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -177,9 +249,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
})
|
})
|
||||||
|
|
||||||
srv = &http.Server{Handler: handlers.CORS(
|
srv = &http.Server{Handler: handlers.CORS(
|
||||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "X-Api-Token", "X-Target-Uri"}),
|
||||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||||
handlers.AllowedOrigins([]string{"*"}),
|
handlers.AllowedOrigins([]string{"*"}),
|
||||||
|
handlers.AllowCredentials(),
|
||||||
)(r)}
|
)(r)}
|
||||||
return srv, listener, nil
|
return srv, listener, nil
|
||||||
}
|
}
|
||||||
@@ -315,3 +388,47 @@ func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
|
|||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
json.NewEncoder(w).Encode(v)
|
json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func aesEncrypt(key, data []byte) (string, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherText := gcm.Seal(nonce, nonce, data, nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(cipherText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func aesDecrypt(key []byte, encryptedData string) ([]byte, error) {
|
||||||
|
cipherText, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cipherText) < gcm.NonceSize() {
|
||||||
|
return nil, errors.New("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, cipherText := cipherText[:gcm.NonceSize()], cipherText[gcm.NonceSize():]
|
||||||
|
return gcm.Open(nil, nonce, cipherText, nil)
|
||||||
|
}
|
||||||
+98
-8
@@ -16,6 +16,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -140,11 +141,13 @@ func TestCreateDirectTask(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateDirectTaskBatch(t *testing.T) {
|
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
reqs = append(reqs, createReq.Req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: createReq.Req,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
|
||||||
Reqs: reqs,
|
Reqs: reqs,
|
||||||
})
|
})
|
||||||
if len(taskIds) != len(reqs) {
|
if len(taskIds) != len(reqs) {
|
||||||
@@ -153,6 +156,45 @@ func TestCreateDirectTaskBatch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateDirectTaskBatchWithOpt(t *testing.T) {
|
||||||
|
doTest(func() {
|
||||||
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
item := &base.CreateTaskBatchItem{
|
||||||
|
Req: createReq.Req,
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
item.Opts = &base.Options{
|
||||||
|
Name: "spe_opt.data",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reqs = append(reqs, item)
|
||||||
|
}
|
||||||
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
|
||||||
|
Reqs: reqs,
|
||||||
|
Opts: &base.Options{
|
||||||
|
Name: "default_opt.data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if len(taskIds) != len(reqs) {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, taskId := range taskIds {
|
||||||
|
task := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||||
|
if i == 0 {
|
||||||
|
if !strings.Contains(task.Name(), "spe_opt") {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "spe_opt.data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(task.Name(), "default_opt") {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "default_opt.data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -595,7 +637,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
cfg.Init()
|
cfg.Init()
|
||||||
cfg.ApiToken = "123456"
|
cfg.ApiToken = "123456"
|
||||||
cfg.WebEnable = true
|
cfg.WebEnable = true
|
||||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
cfg.WebAuth = &model.WebAuth{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Password: "123456",
|
Password: "123456",
|
||||||
}
|
}
|
||||||
@@ -607,14 +649,61 @@ func TestAuthorization(t *testing.T) {
|
|||||||
Stop()
|
Stop()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
status, _ := doHttpRequest0(http.MethodPost, "/api/web/login", nil, &model.WebAuth{
|
||||||
|
Username: "xxx",
|
||||||
|
Password: "xxx",
|
||||||
|
})
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := httpRequestCheckOk[string](http.MethodPost, "/api/web/login", cfg.WebAuth)
|
||||||
|
authToken := fmt.Sprintf("Bearer %s", token)
|
||||||
|
authHeaders := map[string]string{
|
||||||
|
"Authorization": authToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||||
if status != http.StatusUnauthorized {
|
if status != http.StatusUnauthorized {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
"Authorization": "xxx",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": "xxx",
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildToken := func(username, password string, ts int64) string {
|
||||||
|
token, _ := aesEncrypt(aesKey, []byte(fmt.Sprintf("%s:%s:%d", username, password, ts)))
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeToken := buildToken("fake", "fake", time.Now().Unix())
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", fakeToken),
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
expireToken := buildToken(cfg.WebAuth.Username, cfg.WebAuth.Password, time.Now().Add(-time.Hour*8*24).Unix())
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", expireToken),
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", authHeaders, nil)
|
||||||
if status != http.StatusOK {
|
if status != http.StatusOK {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -627,7 +716,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
"Authorization": authToken,
|
||||||
"X-Api-Token": cfg.ApiToken,
|
"X-Api-Token": cfg.ApiToken,
|
||||||
}, nil)
|
}, nil)
|
||||||
if status != http.StatusOK {
|
if status != http.StatusOK {
|
||||||
@@ -635,7 +724,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
"Authorization": authToken,
|
||||||
"X-Api-Token": "",
|
"X-Api-Token": "",
|
||||||
}, nil)
|
}, nil)
|
||||||
if status != http.StatusUnauthorized {
|
if status != http.StatusUnauthorized {
|
||||||
@@ -666,6 +755,7 @@ func doTest0(onStart func(cfg *model.StartConfig), handler func()) {
|
|||||||
Downloader.Clear()
|
Downloader.Clear()
|
||||||
}()
|
}()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
Downloader.Pause(nil)
|
Downloader.Pause(nil)
|
||||||
Downloader.Delete(nil, true)
|
Downloader.Delete(nil, true)
|
||||||
os.RemoveAll(cfg.StorageDir)
|
os.RemoveAll(cfg.StorageDir)
|
||||||
|
|||||||
@@ -56,3 +56,13 @@ func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error)
|
|||||||
}
|
}
|
||||||
return http.ProxyURL(proxyUrl)
|
return http.ProxyURL(proxyUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TryUrlQueryUnescape tries to unescape a URL-encoded string.
|
||||||
|
//
|
||||||
|
// If unescaping fails, it returns the original string.
|
||||||
|
func TryUrlQueryUnescape(s string) string {
|
||||||
|
if decoded, err := url.QueryUnescape(s); err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -128,3 +128,24 @@ func TestParseDataUri(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTryURLDecode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"normal.txt", "normal.txt"},
|
||||||
|
{"%E7%8F%80%E5%B0%94%E8%AF%BA.zip", "珀尔诺.zip"},
|
||||||
|
{"hello%20world.txt", "hello world.txt"},
|
||||||
|
{"bad%2-text", "bad%2-text"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
got := TryUrlQueryUnescape(tt.input)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("TryUrlQueryUnescape(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-32
@@ -3,19 +3,24 @@ import 'dart:io';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:gopeed/api/model/extension.dart';
|
import 'package:get/get.dart' as getx;
|
||||||
import 'package:gopeed/api/model/install_extension.dart';
|
|
||||||
import 'package:gopeed/api/model/switch_extension.dart';
|
|
||||||
|
|
||||||
|
import '../app/routes/app_pages.dart';
|
||||||
|
import '../database/database.dart';
|
||||||
import '../util/util.dart';
|
import '../util/util.dart';
|
||||||
import 'model/create_task.dart';
|
import 'model/create_task.dart';
|
||||||
|
import 'model/create_task_batch.dart';
|
||||||
import 'model/downloader_config.dart';
|
import 'model/downloader_config.dart';
|
||||||
|
import 'model/extension.dart';
|
||||||
|
import 'model/install_extension.dart';
|
||||||
|
import 'model/login.dart';
|
||||||
import 'model/request.dart';
|
import 'model/request.dart';
|
||||||
import 'model/resolve_result.dart';
|
import 'model/resolve_result.dart';
|
||||||
import 'model/result.dart';
|
import 'model/result.dart';
|
||||||
|
import 'model/switch_extension.dart';
|
||||||
import 'model/task.dart';
|
import 'model/task.dart';
|
||||||
import 'model/update_extension_settings.dart';
|
|
||||||
import 'model/update_check_extension_resp.dart';
|
import 'model/update_check_extension_resp.dart';
|
||||||
|
import 'model/update_extension_settings.dart';
|
||||||
|
|
||||||
class _Client {
|
class _Client {
|
||||||
static _Client? _instance;
|
static _Client? _instance;
|
||||||
@@ -29,12 +34,12 @@ class _Client {
|
|||||||
_instance = _Client._internal();
|
_instance = _Client._internal();
|
||||||
var dio = Dio();
|
var dio = Dio();
|
||||||
final isUnixSocket = network == 'unix';
|
final isUnixSocket = network == 'unix';
|
||||||
var baseUrl = 'http://127.0.0.1';
|
var baseUrl = 'http://127.0.0.1/';
|
||||||
if (!isUnixSocket) {
|
if (!isUnixSocket) {
|
||||||
if (Util.isWeb()) {
|
if (Util.isWeb()) {
|
||||||
baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : '';
|
baseUrl = kDebugMode ? 'http://127.0.0.1:9999/' : '';
|
||||||
} else {
|
} else {
|
||||||
baseUrl = 'http://$address';
|
baseUrl = 'http://$address/';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dio.options.baseUrl = baseUrl;
|
dio.options.baseUrl = baseUrl;
|
||||||
@@ -42,12 +47,27 @@ class _Client {
|
|||||||
dio.options.sendTimeout = const Duration(seconds: 5);
|
dio.options.sendTimeout = const Duration(seconds: 5);
|
||||||
dio.options.connectTimeout = const Duration(seconds: 5);
|
dio.options.connectTimeout = const Duration(seconds: 5);
|
||||||
dio.options.receiveTimeout = const Duration(seconds: 60);
|
dio.options.receiveTimeout = const Duration(seconds: 60);
|
||||||
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
|
dio.interceptors.add(InterceptorsWrapper(
|
||||||
if (apiToken.isNotEmpty) {
|
onRequest: (options, handler) {
|
||||||
options.headers['X-Api-Token'] = apiToken;
|
if (apiToken.isNotEmpty) {
|
||||||
}
|
options.headers['X-Api-Token'] = apiToken;
|
||||||
handler.next(options);
|
}
|
||||||
}));
|
if (Util.isWeb()) {
|
||||||
|
final token = Database.instance.getWebToken();
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.next(options);
|
||||||
|
},
|
||||||
|
onError: (error, handler) {
|
||||||
|
// Only web version has a login page
|
||||||
|
if (Util.isWeb() && error.response?.statusCode == 401) {
|
||||||
|
getx.Get.rootDelegate.offAndToNamed(Routes.LOGIN);
|
||||||
|
}
|
||||||
|
handler.next(error);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
_instance!.dio = dio;
|
_instance!.dio = dio;
|
||||||
if (isUnixSocket) {
|
if (isUnixSocket) {
|
||||||
@@ -95,7 +115,8 @@ Future<T> _parse<T>(
|
|||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.type == DioExceptionType.sendTimeout ||
|
if (e.type == DioExceptionType.sendTimeout ||
|
||||||
e.type == DioExceptionType.receiveTimeout ||
|
e.type == DioExceptionType.receiveTimeout ||
|
||||||
e.type == DioExceptionType.connectionTimeout) {
|
e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
throw TimeoutException("request timeout");
|
throw TimeoutException("request timeout");
|
||||||
}
|
}
|
||||||
throw Exception(Result(code: 1000, msg: e.message));
|
throw Exception(Result(code: 1000, msg: e.message));
|
||||||
@@ -104,16 +125,22 @@ Future<T> _parse<T>(
|
|||||||
|
|
||||||
Future<ResolveResult> resolve(Request request) async {
|
Future<ResolveResult> resolve(Request request) async {
|
||||||
return _parse<ResolveResult>(
|
return _parse<ResolveResult>(
|
||||||
() => _client.dio.post("/api/v1/resolve", data: request),
|
() => _client.dio.post("api/v1/resolve", data: request),
|
||||||
(data) => ResolveResult.fromJson(data));
|
(data) => ResolveResult.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> createTask(CreateTask createTask) async {
|
Future<String> createTask(CreateTask createTask) async {
|
||||||
return _parse<String>(
|
return _parse<String>(
|
||||||
() => _client.dio.post("/api/v1/tasks", data: createTask),
|
() => _client.dio.post("api/v1/tasks", data: createTask),
|
||||||
(data) => data as String);
|
(data) => data as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> createTaskBatch(CreateTaskBatch createTaskBatch) async {
|
||||||
|
return _parse<List<String>>(
|
||||||
|
() => _client.dio.post("api/v1/tasks/batch", data: createTaskBatch),
|
||||||
|
(data) => (data as List).map((e) => e as String).toList());
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Task>> getTasks(List<Status> statuses) async {
|
Future<List<Task>> getTasks(List<Status> statuses) async {
|
||||||
return _parse<List<Task>>(
|
return _parse<List<Task>>(
|
||||||
() => _client.dio.get(
|
() => _client.dio.get(
|
||||||
@@ -122,16 +149,16 @@ Future<List<Task>> getTasks(List<Status> statuses) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseTask(String id) async {
|
Future<void> pauseTask(String id) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/pause"), null);
|
return _parse(() => _client.dio.put("api/v1/tasks/$id/pause"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> continueTask(String id) async {
|
Future<void> continueTask(String id) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
return _parse(() => _client.dio.put("api/v1/tasks/$id/continue"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseAllTasks(List<String>? ids) async {
|
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
|
() => _client.dio.put("api/v1/tasks/pause", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
}),
|
}),
|
||||||
null);
|
null);
|
||||||
@@ -139,7 +166,7 @@ Future<void> pauseAllTasks(List<String>? ids) async {
|
|||||||
|
|
||||||
Future<void> continueAllTasks(List<String>? ids) async {
|
Future<void> continueAllTasks(List<String>? ids) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
|
() => _client.dio.put("api/v1/tasks/continue", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
}),
|
}),
|
||||||
null);
|
null);
|
||||||
@@ -147,12 +174,12 @@ Future<void> continueAllTasks(List<String>? ids) async {
|
|||||||
|
|
||||||
Future<void> deleteTask(String id, bool force) async {
|
Future<void> deleteTask(String id, bool force) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
() => _client.dio.delete("api/v1/tasks/$id?force=$force"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
|
() => _client.dio.delete("api/v1/tasks", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
"force": force,
|
"force": force,
|
||||||
}),
|
}),
|
||||||
@@ -160,29 +187,29 @@ Future<void> deleteTasks(List<String>? ids, bool force) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloaderConfig> getConfig() async {
|
Future<DownloaderConfig> getConfig() async {
|
||||||
return _parse(() => _client.dio.get("/api/v1/config"),
|
return _parse(() => _client.dio.get("api/v1/config"),
|
||||||
(data) => DownloaderConfig.fromJson(data));
|
(data) => DownloaderConfig.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> putConfig(DownloaderConfig config) async {
|
Future<void> putConfig(DownloaderConfig config) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/config", data: config), null);
|
return _parse(() => _client.dio.put("api/v1/config", data: config), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> installExtension(InstallExtension installExtension) async {
|
Future<void> installExtension(InstallExtension installExtension) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.post("/api/v1/extensions", data: installExtension),
|
() => _client.dio.post("api/v1/extensions", data: installExtension),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Extension>> getExtensions() async {
|
Future<List<Extension>> getExtensions() async {
|
||||||
return _parse<List<Extension>>(() => _client.dio.get("/api/v1/extensions"),
|
return _parse<List<Extension>>(() => _client.dio.get("api/v1/extensions"),
|
||||||
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateExtensionSettings(
|
Future<void> updateExtensionSettings(
|
||||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/extensions/$identity/settings",
|
() => _client.dio.put("api/v1/extensions/$identity/settings",
|
||||||
data: updateExtensionSettings),
|
data: updateExtensionSettings),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
@@ -191,22 +218,27 @@ Future<void> switchExtension(
|
|||||||
String identity, SwitchExtension switchExtension) async {
|
String identity, SwitchExtension switchExtension) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio
|
() => _client.dio
|
||||||
.put("/api/v1/extensions/$identity/switch", data: switchExtension),
|
.put("api/v1/extensions/$identity/switch", data: switchExtension),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteExtension(String identity) async {
|
Future<void> deleteExtension(String identity) async {
|
||||||
return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null);
|
return _parse(() => _client.dio.delete("api/v1/extensions/$identity"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
||||||
return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"),
|
return _parse(() => _client.dio.get("api/v1/extensions/$identity/update"),
|
||||||
(data) => UpdateCheckExtensionResp.fromJson(data));
|
(data) => UpdateCheckExtensionResp.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateExtension(String identity) async {
|
Future<void> updateExtension(String identity) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.post("/api/v1/extensions/$identity/update"), null);
|
() => _client.dio.post("api/v1/extensions/$identity/update"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> login(LoginReq loginReq) async {
|
||||||
|
return _parse(() => _client.dio.post("api/web/login", data: loginReq),
|
||||||
|
(data) => data as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response<String>> proxyRequest<T>(String uri,
|
Future<Response<String>> proxyRequest<T>(String uri,
|
||||||
@@ -223,5 +255,9 @@ Future<Response<String>> proxyRequest<T>(String uri,
|
|||||||
}
|
}
|
||||||
|
|
||||||
String join(String path) {
|
String join(String path) {
|
||||||
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}";
|
final baseUrl = _client.dio.options.baseUrl;
|
||||||
|
final cleanBaseUrl = baseUrl.endsWith('/')
|
||||||
|
? baseUrl.substring(0, baseUrl.length - 1)
|
||||||
|
: baseUrl;
|
||||||
|
return "$cleanBaseUrl/${Util.cleanPath(path)}";
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'options.dart';
|
||||||
|
import 'request.dart';
|
||||||
|
|
||||||
|
part 'create_task_batch.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(explicitToJson: true)
|
||||||
|
class CreateTaskBatch {
|
||||||
|
List<CreateTaskBatchItem>? reqs;
|
||||||
|
Options? opt;
|
||||||
|
|
||||||
|
CreateTaskBatch({
|
||||||
|
this.reqs,
|
||||||
|
this.opt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateTaskBatch.fromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) =>
|
||||||
|
_$CreateTaskBatchFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateTaskBatchItem {
|
||||||
|
Request? req;
|
||||||
|
Options? opts;
|
||||||
|
|
||||||
|
CreateTaskBatchItem({
|
||||||
|
this.req,
|
||||||
|
this.opts,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateTaskBatchItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CreateTaskBatchItemFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$CreateTaskBatchItemToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'create_task_batch.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CreateTaskBatch _$CreateTaskBatchFromJson(Map<String, dynamic> json) =>
|
||||||
|
CreateTaskBatch(
|
||||||
|
reqs: (json['reqs'] as List<dynamic>?)
|
||||||
|
?.map((e) => CreateTaskBatchItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
opt: json['opt'] == null
|
||||||
|
? null
|
||||||
|
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
|
||||||
|
final val = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('reqs', instance.reqs?.map((e) => e.toJson()).toList());
|
||||||
|
writeNotNull('opt', instance.opt?.toJson());
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateTaskBatchItem _$CreateTaskBatchItemFromJson(Map<String, dynamic> json) =>
|
||||||
|
CreateTaskBatchItem(
|
||||||
|
req: json['req'] == null
|
||||||
|
? null
|
||||||
|
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||||
|
opts: json['opts'] == null
|
||||||
|
? null
|
||||||
|
: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CreateTaskBatchItemToJson(CreateTaskBatchItem instance) {
|
||||||
|
final val = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('req', instance.req);
|
||||||
|
writeNotNull('opts', instance.opts);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ class ExtraConfig {
|
|||||||
bool lastDeleteTaskKeep;
|
bool lastDeleteTaskKeep;
|
||||||
bool defaultDirectDownload;
|
bool defaultDirectDownload;
|
||||||
bool defaultBtClient;
|
bool defaultBtClient;
|
||||||
|
bool notifyWhenNewVersion;
|
||||||
|
|
||||||
ExtraConfigBt bt = ExtraConfigBt();
|
ExtraConfigBt bt = ExtraConfigBt();
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ class ExtraConfig {
|
|||||||
this.lastDeleteTaskKeep = false,
|
this.lastDeleteTaskKeep = false,
|
||||||
this.defaultDirectDownload = false,
|
this.defaultDirectDownload = false,
|
||||||
this.defaultBtClient = true,
|
this.defaultBtClient = true,
|
||||||
|
this.notifyWhenNewVersion = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
|||||||
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
||||||
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
||||||
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
|
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
|
||||||
|
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
|
||||||
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||||
|
|
||||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||||
@@ -83,6 +84,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
|||||||
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||||
'defaultDirectDownload': instance.defaultDirectDownload,
|
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||||
'defaultBtClient': instance.defaultBtClient,
|
'defaultBtClient': instance.defaultBtClient,
|
||||||
|
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
|
||||||
'bt': instance.bt.toJson(),
|
'bt': instance.bt.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'login.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class LoginReq {
|
||||||
|
String username;
|
||||||
|
String password;
|
||||||
|
|
||||||
|
LoginReq({
|
||||||
|
required this.username,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginReq.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LoginReqFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$LoginReqToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'login.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
LoginReq _$LoginReqFromJson(Map<String, dynamic> json) => LoginReq(
|
||||||
|
username: json['username'] as String,
|
||||||
|
password: json['password'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LoginReqToJson(LoginReq instance) => <String, dynamic>{
|
||||||
|
'username': instance.username,
|
||||||
|
'password': instance.password,
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
|
import '../../../../api/model/create_task.dart';
|
||||||
import '../../../../api/model/downloader_config.dart';
|
import '../../../../api/model/downloader_config.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
import '../../../../core/common/start_config.dart';
|
import '../../../../core/common/start_config.dart';
|
||||||
@@ -31,7 +32,7 @@ import '../../../../util/package_info.dart';
|
|||||||
import '../../../../util/updater.dart';
|
import '../../../../util/updater.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
import '../../create/dto/create_router_params.dart';
|
import '../../../rpc/rpc.dart';
|
||||||
import '../../redirect/views/redirect_view.dart';
|
import '../../redirect/views/redirect_view.dart';
|
||||||
|
|
||||||
const unixSocketPath = 'gopeed.sock';
|
const unixSocketPath = 'gopeed.sock';
|
||||||
@@ -78,6 +79,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
_initTray().onError(
|
_initTray().onError(
|
||||||
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
||||||
|
|
||||||
|
_initRpcServer().onError((error, stackTrace) =>
|
||||||
|
logger.w("initRpcServer error", error, stackTrace));
|
||||||
|
|
||||||
_initForegroundTask().onError((error, stackTrace) =>
|
_initForegroundTask().onError((error, stackTrace) =>
|
||||||
logger.w("initForegroundTask error", error, stackTrace));
|
logger.w("initForegroundTask error", error, stackTrace));
|
||||||
|
|
||||||
@@ -119,7 +123,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onTrayIconRightMouseDown() {
|
void onTrayIconRightMouseDown() {
|
||||||
trayManager.popUpContextMenu();
|
trayManager.popUpContextMenu(bringAppToFront: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -271,6 +275,37 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
trayManager.addListener(this);
|
trayManager.addListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initRpcServer() async {
|
||||||
|
if (!Util.isDesktop()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await startRpcServer({
|
||||||
|
"/create": (ctx) async {
|
||||||
|
final meta =
|
||||||
|
ctx.request.headers["X-Gopeed-Host-Meta"]?.firstOrNull ?? "{}";
|
||||||
|
final jsonMeta = jsonDecode(meta);
|
||||||
|
final silent = jsonMeta['silent'] as bool? ?? false;
|
||||||
|
final params = await ctx.readText();
|
||||||
|
final createTaskParams = _decodeToCreatTaskParams(params);
|
||||||
|
if (!silent) {
|
||||||
|
await windowManager.show();
|
||||||
|
_handleToCreate0(createTaskParams);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await createTask(createTaskParams);
|
||||||
|
} catch (e) {
|
||||||
|
logger.w(
|
||||||
|
"create task from extension fail", e, StackTrace.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.w("start rpc server fail", e, StackTrace.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initForegroundTask() async {
|
Future<void> _initForegroundTask() async {
|
||||||
if (!Util.isMobile()) {
|
if (!Util.isMobile()) {
|
||||||
return;
|
return;
|
||||||
@@ -317,13 +352,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
if (uri.path == "/create") {
|
if (uri.path == "/create") {
|
||||||
final params = uri.queryParameters["params"];
|
final params = uri.queryParameters["params"];
|
||||||
if (params?.isNotEmpty == true) {
|
if (params?.isNotEmpty == true) {
|
||||||
final safeParams = params!.replaceAll(" ", "+");
|
_handleToCreate(params!);
|
||||||
final paramsJson =
|
|
||||||
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
|
|
||||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
|
||||||
arguments: RedirectArgs(Routes.CREATE,
|
|
||||||
arguments:
|
|
||||||
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Get.rootDelegate.offAndToNamed(Routes.CREATE);
|
Get.rootDelegate.offAndToNamed(Routes.CREATE);
|
||||||
@@ -346,7 +375,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
}
|
}
|
||||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||||
arguments: RedirectArgs(Routes.CREATE,
|
arguments: RedirectArgs(Routes.CREATE,
|
||||||
arguments: CreateRouterParams(req: Request(url: path))));
|
arguments: CreateTask(req: Request(url: path))));
|
||||||
}
|
}
|
||||||
|
|
||||||
String runningAddress() {
|
String runningAddress() {
|
||||||
@@ -506,6 +535,11 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initCheckUpdate() async {
|
Future<void> _initCheckUpdate() async {
|
||||||
|
// Check if auto check update is enabled
|
||||||
|
if (!downloaderConfig.value.extra.notifyWhenNewVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final versionInfo = await checkUpdate();
|
final versionInfo = await checkUpdate();
|
||||||
if (versionInfo != null) {
|
if (versionInfo != null) {
|
||||||
await showUpdateDialog(Get.context!, versionInfo);
|
await showUpdateDialog(Get.context!, versionInfo);
|
||||||
@@ -519,4 +553,21 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
apiToken: startConfig.value.apiToken));
|
apiToken: startConfig.value.apiToken));
|
||||||
await putConfig(downloaderConfig.value);
|
await putConfig(downloaderConfig.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateTask _decodeToCreatTaskParams(String params) {
|
||||||
|
final safeParams = params.replaceAll('"', "").replaceAll(" ", "+");
|
||||||
|
final paramsJson =
|
||||||
|
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
|
||||||
|
return CreateTask.fromJson(jsonDecode(paramsJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleToCreate(String params) {
|
||||||
|
final createTaskParams = _decodeToCreatTaskParams(params);
|
||||||
|
_handleToCreate0(createTaskParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleToCreate0(CreateTask createTaskParams) {
|
||||||
|
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||||
|
arguments: RedirectArgs(Routes.CREATE, arguments: createTaskParams));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
import '../../../../api/model/options.dart';
|
|
||||||
import '../../../../api/model/request.dart';
|
|
||||||
|
|
||||||
part 'create_router_params.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(explicitToJson: true)
|
|
||||||
class CreateRouterParams {
|
|
||||||
Request? req;
|
|
||||||
Options? opt;
|
|
||||||
|
|
||||||
CreateRouterParams({
|
|
||||||
this.req,
|
|
||||||
this.opt,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory CreateRouterParams.fromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) =>
|
|
||||||
_$CreateRouterParamsFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$CreateRouterParamsToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'create_router_params.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
CreateRouterParams _$CreateRouterParamsFromJson(Map<String, dynamic> json) =>
|
|
||||||
CreateRouterParams(
|
|
||||||
req: json['req'] == null
|
|
||||||
? null
|
|
||||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
|
||||||
opt: json['opt'] == null
|
|
||||||
? null
|
|
||||||
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CreateRouterParamsToJson(CreateRouterParams instance) {
|
|
||||||
final val = <String, dynamic>{};
|
|
||||||
|
|
||||||
void writeNotNull(String key, dynamic value) {
|
|
||||||
if (value != null) {
|
|
||||||
val[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeNotNull('req', instance.req?.toJson());
|
|
||||||
writeNotNull('opt', instance.opt?.toJson());
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
|||||||
|
|
||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
import '../../../../api/model/create_task.dart';
|
import '../../../../api/model/create_task.dart';
|
||||||
|
import '../../../../api/model/create_task_batch.dart';
|
||||||
import '../../../../api/model/options.dart';
|
import '../../../../api/model/options.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
import '../../../../api/model/resolve_result.dart';
|
import '../../../../api/model/resolve_result.dart';
|
||||||
@@ -26,7 +27,6 @@ import '../../../views/file_tree_view.dart';
|
|||||||
import '../../app/controllers/app_controller.dart';
|
import '../../app/controllers/app_controller.dart';
|
||||||
import '../../history/views/history_view.dart';
|
import '../../history/views/history_view.dart';
|
||||||
import '../controllers/create_controller.dart';
|
import '../controllers/create_controller.dart';
|
||||||
import '../dto/create_router_params.dart';
|
|
||||||
|
|
||||||
class CreateView extends GetView<CreateController> {
|
class CreateView extends GetView<CreateController> {
|
||||||
final _confirmFormKey = GlobalKey<FormState>();
|
final _confirmFormKey = GlobalKey<FormState>();
|
||||||
@@ -75,7 +75,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
|
final CreateTask? routerParams = Get.rootDelegate.arguments();
|
||||||
if (routerParams?.req?.url.isNotEmpty ?? false) {
|
if (routerParams?.req?.url.isNotEmpty ?? false) {
|
||||||
// get url from route arguments
|
// get url from route arguments
|
||||||
final url = routerParams!.req!.url;
|
final url = routerParams!.req!.url;
|
||||||
@@ -869,18 +869,20 @@ class CreateView extends GetView<CreateController> {
|
|||||||
if (createFormKey.currentState!.validate()) {
|
if (createFormKey.currentState!.validate()) {
|
||||||
if (rr.id.isEmpty) {
|
if (rr.id.isEmpty) {
|
||||||
// from extension resolve result
|
// from extension resolve result
|
||||||
await Future.wait(
|
final reqs =
|
||||||
controller.selectedIndexes.map((index) {
|
controller.selectedIndexes.map((index) {
|
||||||
final file = rr.res.files[index];
|
final file = rr.res.files[index];
|
||||||
return createTask(CreateTask(
|
return CreateTaskBatchItem(
|
||||||
req: file.req!..proxy = parseProxy(),
|
req: file.req!..proxy = parseProxy(),
|
||||||
opt: Options(
|
opts: Options(
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: path.join(_pathController.text,
|
path: path.join(_pathController.text,
|
||||||
rr.res.name, file.path),
|
rr.res.name, file.path),
|
||||||
selectFiles: [],
|
selectFiles: [],
|
||||||
extra: optExtra)));
|
extra: optExtra));
|
||||||
}));
|
}).toList();
|
||||||
|
await createTaskBatch(
|
||||||
|
CreateTaskBatch(reqs: reqs));
|
||||||
} else {
|
} else {
|
||||||
await createTask(CreateTask(
|
await createTask(CreateTask(
|
||||||
rid: rr.id,
|
rid: rr.id,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '../../../../api/model/extension.dart';
|
|||||||
import '../../../../api/model/install_extension.dart';
|
import '../../../../api/model/install_extension.dart';
|
||||||
import '../../../../api/model/switch_extension.dart';
|
import '../../../../api/model/switch_extension.dart';
|
||||||
import '../../../../api/model/update_extension_settings.dart';
|
import '../../../../api/model/update_extension_settings.dart';
|
||||||
|
import '../../../../database/database.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../views/icon_button_loading.dart';
|
import '../../../views/icon_button_loading.dart';
|
||||||
@@ -135,6 +136,10 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
|
headers: {
|
||||||
|
'Authorization':
|
||||||
|
'Bearer ${Database.instance.getWebToken()}'
|
||||||
|
},
|
||||||
)
|
)
|
||||||
: Image.file(
|
: Image.file(
|
||||||
extension.devMode
|
extension.devMode
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../controllers/login_controller.dart';
|
||||||
|
|
||||||
|
class LoginBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
Get.lazyPut<LoginController>(
|
||||||
|
() => LoginController(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../../../api/api.dart' as api;
|
||||||
|
import '../../../../api/api.dart';
|
||||||
|
import '../../../../api/model/login.dart';
|
||||||
|
import '../../../../database/database.dart';
|
||||||
|
import '../../../../util/message.dart';
|
||||||
|
import '../../../routes/app_pages.dart';
|
||||||
|
import '../../app/controllers/app_controller.dart';
|
||||||
|
|
||||||
|
class LoginController extends GetxController {
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
final usernameController = TextEditingController();
|
||||||
|
final passwordController = TextEditingController();
|
||||||
|
|
||||||
|
final isLoading = false.obs;
|
||||||
|
final passwordVisible = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
usernameController.dispose();
|
||||||
|
passwordController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void togglePasswordVisibility() {
|
||||||
|
passwordVisible.value = !passwordVisible.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> login() async {
|
||||||
|
if (!formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
final loginReq = LoginReq(
|
||||||
|
username: usernameController.text.trim(),
|
||||||
|
password: passwordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
final token = await api.login(loginReq);
|
||||||
|
// Login successful, save the token
|
||||||
|
Database.instance.saveWebToken(token);
|
||||||
|
// Reload config
|
||||||
|
final controller = Get.put(AppController());
|
||||||
|
await controller.loadDownloaderConfig();
|
||||||
|
// Navigate to home page
|
||||||
|
Get.rootDelegate.offAndToNamed(Routes.HOME);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is TimeoutException) {
|
||||||
|
showMessage('error'.tr, 'login_failed_network'.tr);
|
||||||
|
} else {
|
||||||
|
showMessage('error'.tr, 'login_failed'.tr);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../../views/responsive_builder.dart';
|
||||||
|
import '../controllers/login_controller.dart';
|
||||||
|
|
||||||
|
class LoginView extends GetView<LoginController> {
|
||||||
|
const LoginView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isNarrow = ResponsiveBuilder.isNarrow(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Get.theme.colorScheme.surface,
|
||||||
|
Get.theme.colorScheme.surface.withOpacity(0.8),
|
||||||
|
Get.theme.colorScheme.surface.withOpacity(0.9),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: isNarrow ? 16.0 : 32.0,
|
||||||
|
vertical: isNarrow ? 24.0 : 32.0,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
elevation: 12,
|
||||||
|
shadowColor: Get.theme.colorScheme.shadow.withOpacity(0.3),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(isNarrow ? 20 : 24),
|
||||||
|
),
|
||||||
|
color: Get.theme.colorScheme.surface,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: isNarrow ? double.infinity : 420,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(isNarrow ? 24.0 : 40.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(isNarrow ? 20 : 24),
|
||||||
|
border: Border.all(
|
||||||
|
color: Get.theme.colorScheme.outline.withOpacity(0.1),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: FocusTraversalGroup(
|
||||||
|
policy: OrderedTraversalPolicy(),
|
||||||
|
child: Form(
|
||||||
|
key: controller.formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Logo/Title Section
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
margin: EdgeInsets.only(bottom: isNarrow ? 32 : 48),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(isNarrow ? 8 : 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(isNarrow ? 20 : 24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.2),
|
||||||
|
blurRadius: isNarrow ? 16 : 24,
|
||||||
|
offset: Offset(0, isNarrow ? 8 : 12),
|
||||||
|
spreadRadius: isNarrow ? 1 : 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(isNarrow ? 16 : 20),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/icon/icon.svg',
|
||||||
|
width: isNarrow ? 56 : 72,
|
||||||
|
height: isNarrow ? 56 : 72,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Gopeed',
|
||||||
|
style: Get.textTheme.headlineLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Get.theme.colorScheme.onSurface,
|
||||||
|
letterSpacing: 2.0,
|
||||||
|
fontSize: isNarrow ? 28 : 36,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Username Field
|
||||||
|
FocusTraversalOrder(
|
||||||
|
order: const NumericFocusOrder(1.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller.usernameController,
|
||||||
|
autofillHints: const [
|
||||||
|
AutofillHints.username,
|
||||||
|
],
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onFieldSubmitted: (_) => controller.login(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'username'.tr,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Get.theme.colorScheme.onSurface
|
||||||
|
.withOpacity(0.7),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
prefixIcon: Container(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.person_outline_rounded,
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.3),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Get
|
||||||
|
.theme.colorScheme.surfaceContainerHighest
|
||||||
|
.withOpacity(0.8),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'username_required'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: isNarrow ? 16 : 24),
|
||||||
|
|
||||||
|
// Password Field
|
||||||
|
FocusTraversalOrder(
|
||||||
|
order: const NumericFocusOrder(2.0),
|
||||||
|
child: Obx(() => TextFormField(
|
||||||
|
controller: controller.passwordController,
|
||||||
|
autofillHints: const [
|
||||||
|
AutofillHints.password,
|
||||||
|
],
|
||||||
|
obscureText: !controller.passwordVisible.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'password'.tr,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: Get.theme.colorScheme.onSurface
|
||||||
|
.withOpacity(0.7),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
prefixIcon: Container(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.lock_outline_rounded,
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
controller.passwordVisible.value
|
||||||
|
? Icons.visibility_off_rounded
|
||||||
|
: Icons.visibility_rounded,
|
||||||
|
color: Get.theme.colorScheme.onSurface
|
||||||
|
.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
controller.togglePasswordVisibility,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.outline
|
||||||
|
.withOpacity(0.3),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.primary,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Get.theme.colorScheme.error,
|
||||||
|
width: 2.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Get
|
||||||
|
.theme.colorScheme.surfaceContainerHighest
|
||||||
|
.withOpacity(0.8),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'password_required'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onFieldSubmitted: (_) => controller.login(),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: isNarrow ? 24 : 32),
|
||||||
|
|
||||||
|
// Login Button
|
||||||
|
Obx(() => ElevatedButton(
|
||||||
|
onPressed: controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: controller.login,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: isNarrow ? 16 : 18,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
backgroundColor: controller.isLoading.value
|
||||||
|
? Get.theme.disabledColor
|
||||||
|
: Get.theme.colorScheme.primary,
|
||||||
|
foregroundColor:
|
||||||
|
Get.theme.colorScheme.onPrimary,
|
||||||
|
elevation: controller.isLoading.value ? 0 : 4,
|
||||||
|
shadowColor: Get.theme.colorScheme.primary
|
||||||
|
.withOpacity(0.4),
|
||||||
|
),
|
||||||
|
child: controller.isLoading.value
|
||||||
|
? SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
valueColor:
|
||||||
|
AlwaysStoppedAnimation<Color>(Get
|
||||||
|
.theme.colorScheme.onPrimary),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'login'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -531,6 +531,25 @@ class SettingView extends GetView<SettingController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final buildAutoCheckUpdate = _buildConfigItem(
|
||||||
|
'notifyWhenNewVersion',
|
||||||
|
() =>
|
||||||
|
downloaderCfg.value.extra.notifyWhenNewVersion ? 'on'.tr : 'off'.tr,
|
||||||
|
(Key key) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Switch(
|
||||||
|
value: downloaderCfg.value.extra.notifyWhenNewVersion,
|
||||||
|
onChanged: (bool value) async {
|
||||||
|
downloaderCfg.update((val) {
|
||||||
|
val!.extra.notifyWhenNewVersion = value;
|
||||||
|
});
|
||||||
|
await debounceSave();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
buildThanks() {
|
buildThanks() {
|
||||||
const thankPage =
|
const thankPage =
|
||||||
'https://github.com/GopeedLab/gopeed/graphs/contributors';
|
'https://github.com/GopeedLab/gopeed/graphs/contributors';
|
||||||
@@ -957,6 +976,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
children: _addDivider([
|
children: _addDivider([
|
||||||
buildHomepage(),
|
buildHomepage(),
|
||||||
buildVersion(),
|
buildVersion(),
|
||||||
|
buildAutoCheckUpdate(),
|
||||||
buildThanks(),
|
buildThanks(),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import '../modules/extension/bindings/extension_binding.dart';
|
|||||||
import '../modules/extension/views/extension_view.dart';
|
import '../modules/extension/views/extension_view.dart';
|
||||||
import '../modules/home/bindings/home_binding.dart';
|
import '../modules/home/bindings/home_binding.dart';
|
||||||
import '../modules/home/views/home_view.dart';
|
import '../modules/home/views/home_view.dart';
|
||||||
|
import '../modules/login/bindings/login_binding.dart';
|
||||||
|
import '../modules/login/views/login_view.dart';
|
||||||
import '../modules/redirect/bindings/redirect_binding.dart';
|
import '../modules/redirect/bindings/redirect_binding.dart';
|
||||||
import '../modules/redirect/views/redirect_view.dart';
|
import '../modules/redirect/views/redirect_view.dart';
|
||||||
import '../modules/root/bindings/root_binding.dart';
|
import '../modules/root/bindings/root_binding.dart';
|
||||||
@@ -63,6 +65,12 @@ class AppPages {
|
|||||||
binding: SettingBinding(),
|
binding: SettingBinding(),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
GetPage(
|
||||||
|
name: _Paths.LOGIN,
|
||||||
|
page: () => const LoginView(),
|
||||||
|
binding: LoginBinding(),
|
||||||
|
transition: Transition.fadeIn,
|
||||||
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: _Paths.CREATE,
|
name: _Paths.CREATE,
|
||||||
transition: Transition.downToUp,
|
transition: Transition.downToUp,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ abstract class Routes {
|
|||||||
static const ROOT = _Paths.ROOT;
|
static const ROOT = _Paths.ROOT;
|
||||||
static const HOME = _Paths.HOME;
|
static const HOME = _Paths.HOME;
|
||||||
static const CREATE = _Paths.CREATE;
|
static const CREATE = _Paths.CREATE;
|
||||||
|
static const LOGIN = _Paths.LOGIN;
|
||||||
static const TASK = _Paths.HOME + _Paths.TASK;
|
static const TASK = _Paths.HOME + _Paths.TASK;
|
||||||
static const TASK_FILES = TASK + _Paths.TASK_FILES;
|
static const TASK_FILES = TASK + _Paths.TASK_FILES;
|
||||||
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
|
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
|
||||||
@@ -20,6 +21,7 @@ abstract class _Paths {
|
|||||||
static const ROOT = '/';
|
static const ROOT = '/';
|
||||||
static const HOME = '/home';
|
static const HOME = '/home';
|
||||||
static const CREATE = '/create';
|
static const CREATE = '/create';
|
||||||
|
static const LOGIN = '/login';
|
||||||
static const TASK = '/task';
|
static const TASK = '/task';
|
||||||
static const TASK_FILES = '/files';
|
static const TASK_FILES = '/files';
|
||||||
static const EXTENSION = '/extension';
|
static const EXTENSION = '/extension';
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dart_ipc/dart_ipc.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
import '../../util/util.dart';
|
||||||
|
|
||||||
|
/// RPC context class that contains request information and response methods
|
||||||
|
class RpcContext {
|
||||||
|
final HttpRequest request;
|
||||||
|
final HttpResponse response;
|
||||||
|
String? _bodyCache;
|
||||||
|
|
||||||
|
RpcContext(this.request, this.response);
|
||||||
|
|
||||||
|
/// Read and parse JSON request body
|
||||||
|
Future<Map<String, dynamic>> readJSON() async {
|
||||||
|
if (_bodyCache == null) {
|
||||||
|
final content = <int>[];
|
||||||
|
await for (var data in request) {
|
||||||
|
content.addAll(data);
|
||||||
|
}
|
||||||
|
_bodyCache = String.fromCharCodes(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_bodyCache!.isEmpty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonDecode(_bodyCache!) as Map<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> readText() async {
|
||||||
|
if (_bodyCache == null) {
|
||||||
|
final content = <int>[];
|
||||||
|
await for (var data in request) {
|
||||||
|
content.addAll(data);
|
||||||
|
}
|
||||||
|
_bodyCache = String.fromCharCodes(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _bodyCache ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write JSON response
|
||||||
|
Future<void> writeJSON(Map<String, dynamic> data) async {
|
||||||
|
response.headers.contentType = ContentType.json;
|
||||||
|
response.write(jsonEncode(data));
|
||||||
|
await response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write error response
|
||||||
|
Future<void> writeError(String message, [int statusCode = 500]) async {
|
||||||
|
response.statusCode = statusCode;
|
||||||
|
await writeJSON({'error': message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route handler type definition
|
||||||
|
typedef RouteHandler = Future<void> Function(RpcContext ctx);
|
||||||
|
|
||||||
|
/// Route registry
|
||||||
|
class RouteRegistry {
|
||||||
|
final Map<String, RouteHandler> _routes = {};
|
||||||
|
|
||||||
|
/// Register a route
|
||||||
|
void register(String path, RouteHandler handler) {
|
||||||
|
_routes[path] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get route handler
|
||||||
|
RouteHandler? getHandler(String path) {
|
||||||
|
return _routes[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all registered routes
|
||||||
|
List<String> get routes => _routes.keys.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start RPC server
|
||||||
|
Future<void> startRpcServer([Map<String, RouteHandler>? routes]) async {
|
||||||
|
String path;
|
||||||
|
if (Util.isWindows()) {
|
||||||
|
path = r'\\.\pipe\gopeed_host';
|
||||||
|
} else {
|
||||||
|
path = await Util.homePathJoin("gopeed_host.sock");
|
||||||
|
// try to delete existing socket file
|
||||||
|
final socketFile = File(path);
|
||||||
|
if (await socketFile.exists()) {
|
||||||
|
try {
|
||||||
|
await socketFile.delete();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create route registry
|
||||||
|
final registry = RouteRegistry();
|
||||||
|
|
||||||
|
// Register provided routes
|
||||||
|
if (routes != null) {
|
||||||
|
routes.forEach((path, handler) {
|
||||||
|
registry.register(path, handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverSocket = await bind(path);
|
||||||
|
final httpServer = HttpServer.listenOn(serverSocket);
|
||||||
|
|
||||||
|
httpServer.forEach((HttpRequest request) async {
|
||||||
|
final ctx = RpcContext(request, request.response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get request path
|
||||||
|
final requestPath = request.uri.path;
|
||||||
|
// Find route handler
|
||||||
|
final handler = registry.getHandler(requestPath);
|
||||||
|
if (handler != null) {
|
||||||
|
// Execute route handler
|
||||||
|
await handler(ctx);
|
||||||
|
} else {
|
||||||
|
// 404 Route not found
|
||||||
|
await ctx.writeError('Route not found: $requestPath', 404);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await ctx.writeError('Internal server error: $e', 500);
|
||||||
|
} finally {
|
||||||
|
await request.response.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,10 +6,10 @@ import 'package:hive/hive.dart';
|
|||||||
import 'entity.dart';
|
import 'entity.dart';
|
||||||
|
|
||||||
const String _startConfig = 'startConfig';
|
const String _startConfig = 'startConfig';
|
||||||
const String _lastRunningConfig = 'lastRunningConfig';
|
|
||||||
const String _windowState = 'windowState';
|
const String _windowState = 'windowState';
|
||||||
const String _bookmark = 'bookmark';
|
const String _bookmark = 'bookmark';
|
||||||
const String _createHistory = 'createHistory';
|
const String _createHistory = 'createHistory';
|
||||||
|
const String _webToken = 'webToken';
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
static final Database _instance = Database._internal();
|
static final Database _instance = Database._internal();
|
||||||
@@ -82,6 +82,14 @@ class Database {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void saveWebToken(String token) {
|
||||||
|
save<String>(_webToken, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getWebToken() {
|
||||||
|
return get<String>(_webToken, (json) => json.toString());
|
||||||
|
}
|
||||||
|
|
||||||
void saveCreateHistory(String url) {
|
void saveCreateHistory(String url) {
|
||||||
var list = getCreateHistory() ?? [];
|
var list = getCreateHistory() ?? [];
|
||||||
list.remove(url);
|
list.remove(url);
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
const deDE = {
|
||||||
|
'de_DE': {
|
||||||
|
'label': 'Deutsch',
|
||||||
|
'error': 'Fehler',
|
||||||
|
'tip': 'Tipp',
|
||||||
|
'confirm': 'Bestätigen',
|
||||||
|
'cancel': 'Abbrechen',
|
||||||
|
'on': 'An',
|
||||||
|
'off': 'Aus',
|
||||||
|
'selectAll': 'Alle Auswählen',
|
||||||
|
'select': 'Auswählen',
|
||||||
|
'task': 'Aufgaben',
|
||||||
|
'downloading': 'Wird Heruntergeladen',
|
||||||
|
'downloaded': 'Heruntergeladen',
|
||||||
|
'setting': 'Einstellungen',
|
||||||
|
'donate': 'Spenden',
|
||||||
|
'exit': 'Beenden',
|
||||||
|
'create': 'Aufgabe Erstellen',
|
||||||
|
'directDownload': 'Direkter Download',
|
||||||
|
'advancedOptions': 'Erweiterte Einstellungen',
|
||||||
|
'followSettings': 'Einstellungen Folgen',
|
||||||
|
'downloadLink': 'Download Link',
|
||||||
|
'downloadLinkValid': 'Bitte geben Sie den Download-Link ein',
|
||||||
|
'downloadLinkHit': 'Bitte geben Sie den Download-Link ein, einen pro Zeile@append',
|
||||||
|
'downloadLinkHitDesktop': ', oder ziehen Sie die Torrent-Datei direkt hierher',
|
||||||
|
'download': 'Download',
|
||||||
|
'noFileSelected': 'Bitte wählen Sie mindestens eine Datei aus, um fortzufahren.',
|
||||||
|
'noStoragePermission': 'Speicherberechtigung erforderlich',
|
||||||
|
'selectFile': 'Datei auswählen',
|
||||||
|
'rename': 'Umbenennen',
|
||||||
|
'basic': 'Basic',
|
||||||
|
'advanced': 'Fortschrittlich',
|
||||||
|
'general': 'Allgemein',
|
||||||
|
'downloadDir': 'Download-Verzeichnis',
|
||||||
|
'downloadDirValid': 'Bitte wählen Sie das Download-Verzeichnis',
|
||||||
|
'connections': 'Anschlüsse',
|
||||||
|
'useServerCtime': 'Serverzeit für die Dateierstellung verwenden',
|
||||||
|
'maxRunning': 'Maximale Anzahl ausgeführter Aufgaben',
|
||||||
|
'defaultDirectDownload': 'Direkten Download standardmäßig aktivieren',
|
||||||
|
'items': '@count Artikel',
|
||||||
|
'subscribeTracker': 'Abonnement-Tracker',
|
||||||
|
'subscribeFail':
|
||||||
|
'Anmeldung fehlgeschlagen. Bitte überprüfen Sie das Netzwerk oder versuchen Sie es später erneut.',
|
||||||
|
'update': 'Aktualisieren',
|
||||||
|
'updateDaily': 'Tägliche Aktualisierung',
|
||||||
|
'lastUpdate': 'Letzte Aktualisierung: @time',
|
||||||
|
'addTracker': 'Tracker hinzufügen',
|
||||||
|
'addTrackerHit': 'Bitte geben Sie die URL des Tracker-Servers ein, eine pro Zeile',
|
||||||
|
'ui': 'Benutzeroberfläche',
|
||||||
|
'theme': 'Thema',
|
||||||
|
'themeSystem': 'System',
|
||||||
|
'themeLight': 'Lichtmodus',
|
||||||
|
'themeDark': 'Dunkler Modus',
|
||||||
|
'locale': 'Sprache',
|
||||||
|
'about': 'Über',
|
||||||
|
'homepage': 'Homepage',
|
||||||
|
'version': 'Version',
|
||||||
|
'protocol': 'Protokoll',
|
||||||
|
'port': 'Port',
|
||||||
|
'apiToken': 'API Token',
|
||||||
|
'notSet': 'NS',
|
||||||
|
'set': 'SET',
|
||||||
|
'portInUse': 'Port [@port] wird verwendet, bitte ändern Sie den Port',
|
||||||
|
'effectAfterRestart': 'Wirkung nach Neustart',
|
||||||
|
'developer': 'Entwickler-in',
|
||||||
|
'logDirectory': 'Protokollverzeichnis',
|
||||||
|
'show': 'Zeigen',
|
||||||
|
'continue': 'Weitermachen',
|
||||||
|
'pause': 'Pause',
|
||||||
|
'startAll': 'Alles starten',
|
||||||
|
'pauseAll': 'Alle pausieren',
|
||||||
|
'deleteTask': '@count Aufgaben löschen',
|
||||||
|
'deleteTaskTip': 'Heruntergeladene Dateien behalten',
|
||||||
|
'delete': 'Löschen',
|
||||||
|
'newVersionTitle': 'Entdecken Sie die neue Version @version',
|
||||||
|
'newVersionUpdate': 'Jetzt aktualisieren',
|
||||||
|
'newVersionLater': 'Später',
|
||||||
|
'extensions': 'Erweiterungen',
|
||||||
|
'extensionInstallUrl': 'Installations-URL',
|
||||||
|
'extensionInstallSuccess': 'Erfolgreich installiert',
|
||||||
|
'extensionUpdateSuccess': 'Erfolgreich aktualisiert',
|
||||||
|
'extensionDelete': 'Erweiterung löschen',
|
||||||
|
'extensionAlreadyLatest': 'Es ist bereits die neueste Version',
|
||||||
|
'extensionFind': 'Erweiterungen suchen',
|
||||||
|
'extensionDevelop': 'Erweiterungen entwickeln',
|
||||||
|
'history': 'Verlauf',
|
||||||
|
'clearHistory': 'Verlauf löschen',
|
||||||
|
'noHistoryFound': 'Kein Verlauf gefunden',
|
||||||
|
'serviceTitle': 'Download-Service',
|
||||||
|
'serviceText': 'Läuft',
|
||||||
|
'network': 'Netzwerk',
|
||||||
|
'proxy': 'Proxy',
|
||||||
|
'noProxy': 'Kein Proxy',
|
||||||
|
'systemProxy': 'System-Proxy',
|
||||||
|
'customProxy': 'Benutzerdefinierter Proxy',
|
||||||
|
'server': 'Server',
|
||||||
|
'username': 'Benutzername',
|
||||||
|
'password': 'Passwort',
|
||||||
|
'thanks': 'Danke',
|
||||||
|
'thanksDesc':
|
||||||
|
'Vielen Dank an alle Mitwirkenden, die beim Aufbau und der Entwicklung der Gopeed-Community geholfen haben!',
|
||||||
|
'browserExtension': 'Browsererweiterung',
|
||||||
|
'launchAtStartup': 'Beim Start starten',
|
||||||
|
'seedConfig': 'Seed-Konfiguration',
|
||||||
|
'seedKeep': 'Weiter-seeden bis manuell gestoppt',
|
||||||
|
'seedRatio': 'Seed Verhältniss',
|
||||||
|
'seedTime': 'Seed-Zeit (Minuten)',
|
||||||
|
'setAsDefaultBtClient': 'Als Standard-BT-Client festlegen',
|
||||||
|
'taskDetail': 'Aufgabendetails',
|
||||||
|
'taskName': 'Aufgabenname',
|
||||||
|
'taskUrl': 'Aufgaben-URL',
|
||||||
|
'downloadPath': 'Download-Pfad',
|
||||||
|
'skipVerifyCert': 'Zertifikatsüberprüfung überspringen',
|
||||||
|
'name': 'Name',
|
||||||
|
'size': 'Größe',
|
||||||
|
'unknown': 'Unbekannt',
|
||||||
|
'fileSelectedCount': 'Dateien: ',
|
||||||
|
'fileSelectedSize': 'Größe: ',
|
||||||
|
'httpHeaderName': 'Header-Name',
|
||||||
|
'httpHeaderValue': 'Header-Wert',
|
||||||
|
'login': 'Login',
|
||||||
|
'username_required': 'Bitte geben Sie Ihren Benutzernamen ein',
|
||||||
|
'password_required': 'Bitte geben Sie Ihr Passwort ein',
|
||||||
|
'login_success': 'Anmeldung erfolgreich',
|
||||||
|
'login_failed': 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort',
|
||||||
|
'login_failed_network':
|
||||||
|
'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihre Netzwerkverbindung',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -52,6 +52,7 @@ const enUS = {
|
|||||||
'themeLight': 'Light',
|
'themeLight': 'Light',
|
||||||
'themeDark': 'Dark',
|
'themeDark': 'Dark',
|
||||||
'locale': 'Language',
|
'locale': 'Language',
|
||||||
|
'notifyWhenNewVersion': 'Notify for updates',
|
||||||
'about': 'About',
|
'about': 'About',
|
||||||
'homepage': 'Homepage',
|
'homepage': 'Homepage',
|
||||||
'version': 'Version',
|
'version': 'Version',
|
||||||
@@ -118,5 +119,12 @@ const enUS = {
|
|||||||
'fileSelectedSize': 'Size: ',
|
'fileSelectedSize': 'Size: ',
|
||||||
'httpHeaderName': 'Header Name',
|
'httpHeaderName': 'Header Name',
|
||||||
'httpHeaderValue': 'Header Value',
|
'httpHeaderValue': 'Header Value',
|
||||||
|
'login': 'Login',
|
||||||
|
'username_required': 'Please enter your username',
|
||||||
|
'password_required': 'Please enter your password',
|
||||||
|
'login_success': 'Login successful',
|
||||||
|
'login_failed': 'Login failed, please check your username and password',
|
||||||
|
'login_failed_network':
|
||||||
|
'Login failed, please check your network connection',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
const ptBR = {
|
||||||
|
'pt_BR': {
|
||||||
|
'label': 'Português(Brasil)',
|
||||||
|
'error': 'Erro',
|
||||||
|
'tip': 'Dica',
|
||||||
|
'confirm': 'Confirmar',
|
||||||
|
'cancel': 'Cancelar',
|
||||||
|
'on': 'Ligado',
|
||||||
|
'off': 'Desligado',
|
||||||
|
'selectAll': 'Selecionar Tudo',
|
||||||
|
'select': 'Selecionar',
|
||||||
|
'task': 'Tarefas',
|
||||||
|
'downloading': 'baixando',
|
||||||
|
'downloaded': 'baixado',
|
||||||
|
'setting': 'Configurações',
|
||||||
|
'donate': 'Doação',
|
||||||
|
'exit': 'Sair',
|
||||||
|
'create': 'Criar Tarefa',
|
||||||
|
'directDownload': 'Download Direto',
|
||||||
|
'advancedOptions': 'Opções Avançadas',
|
||||||
|
'followSettings': 'Follow Settings',
|
||||||
|
'downloadLink': 'Link de Download',
|
||||||
|
'downloadLinkValid': 'Por favor, insira o link de download',
|
||||||
|
'downloadLinkHit': 'Por favor, insira o link de download, um por linha@append',
|
||||||
|
'downloadLinkHitDesktop': ', ou arraste o arquivo torrent aqui',
|
||||||
|
'download': 'Download',
|
||||||
|
'noFileSelected': 'Por favor, selecione ao menos um arquivo para continuar',
|
||||||
|
'noStoragePermission': 'Permissão de Armazenamento Necessária',
|
||||||
|
'selectFile': 'Selecionar Arquivo',
|
||||||
|
'rename': 'Renomear para',
|
||||||
|
'basic': 'Configurações Básica',
|
||||||
|
'advanced': 'Configurações Avançada',
|
||||||
|
'general': 'Principal',
|
||||||
|
'downloadDir': 'Onde você deseja salvar?',
|
||||||
|
'downloadDirValid': 'Por favor, selecione a pasta',
|
||||||
|
'connections': 'Conexões',
|
||||||
|
'useServerCtime': 'Use server time for file creation',
|
||||||
|
'maxRunning': 'Máximo de Tarefas Simultâneas',
|
||||||
|
'defaultDirectDownload': 'Marcar download direto por padrão',
|
||||||
|
'items': '@count itens',
|
||||||
|
'subscribeTracker': 'Assinar Rastreador',
|
||||||
|
'subscribeFail':
|
||||||
|
'Falha na Assinatura, verifique sua conexão ou tente novamente mais tarde',
|
||||||
|
'update': 'Atualizar',
|
||||||
|
'updateDaily': 'Atualizar Diariamente',
|
||||||
|
'lastUpdate': 'Última Atualização: @time',
|
||||||
|
'addTracker': 'Adicionar Rastreador',
|
||||||
|
'addTrackerHit': 'Por favor, insira a URL do rastreador, um por linha',
|
||||||
|
'ui': 'Interface',
|
||||||
|
'theme': 'Tema',
|
||||||
|
'themeSystem': 'Sistema',
|
||||||
|
'themeLight': 'Claro',
|
||||||
|
'themeDark': 'Escuro',
|
||||||
|
'locale': 'Idioma',
|
||||||
|
'about': 'Sobre',
|
||||||
|
'homepage': 'Site',
|
||||||
|
'version': 'Versão',
|
||||||
|
'protocol': 'Protocolo',
|
||||||
|
'port': 'Porta',
|
||||||
|
'apiToken': 'API Token',
|
||||||
|
'notSet': 'NS',
|
||||||
|
'set': 'SET',
|
||||||
|
'portInUse': 'Porta [@port] em uso, por favor utilize outra porta',
|
||||||
|
'effectAfterRestart': 'Effect after restart',
|
||||||
|
'developer': 'Desenvolvedor',
|
||||||
|
'logDirectory': 'Pasta de Logs',
|
||||||
|
'show': 'Mostrar',
|
||||||
|
'continue': 'Continuar',
|
||||||
|
'pause': 'Pausar',
|
||||||
|
'startAll': 'Iniciar Tudo',
|
||||||
|
'pauseAll': 'Pausar Tudo',
|
||||||
|
'deleteTask': 'Excluir @count tarefas',
|
||||||
|
'deleteTaskTip': 'Manter arquivos baixados',
|
||||||
|
'delete': 'Excluir',
|
||||||
|
'newVersionTitle': 'Nova Versão Disponível @version',
|
||||||
|
'newVersionUpdate': 'Atualizar Agora',
|
||||||
|
'newVersionLater': 'Mais Tarde',
|
||||||
|
'extensions': 'Extensões',
|
||||||
|
'extensionInstallUrl': 'Link de Instalação',
|
||||||
|
'extensionInstallSuccess': 'Instalado com Sucesso',
|
||||||
|
'extensionUpdateSuccess': 'Atualizado com Sucesso',
|
||||||
|
'extensionDelete': 'Excluir Extensão',
|
||||||
|
'extensionAlreadyLatest': 'It\'s already the latest version',
|
||||||
|
'extensionFind': 'Encontrar Extensões',
|
||||||
|
'extensionDevelop': 'Desenvolvedor',
|
||||||
|
'history': 'Histórico',
|
||||||
|
'clearHistory': 'Limpar Histórico',
|
||||||
|
'noHistoryFound': 'Nenhum Histório Encontrado',
|
||||||
|
'serviceTitle': 'Download Service',
|
||||||
|
'serviceText': 'Executando',
|
||||||
|
'network': 'Rede',
|
||||||
|
'proxy': 'Proxy',
|
||||||
|
'noProxy': 'Sem Proxy',
|
||||||
|
'systemProxy': 'Proxy do Sistema',
|
||||||
|
'customProxy': 'Proxy Personalizado',
|
||||||
|
'server': 'Servidor',
|
||||||
|
'username': 'Usuário',
|
||||||
|
'password': 'Senha',
|
||||||
|
'thanks': 'Obrigado',
|
||||||
|
'thanksDesc':
|
||||||
|
'Obrigado a todos os colaboradores que ajudaram a construir e desenvolver a comunidade Gopeed!',
|
||||||
|
'browserExtension': 'Extensões para Navegadores',
|
||||||
|
'launchAtStartup': 'Launch at Startup',
|
||||||
|
'seedConfig': 'Seed Config',
|
||||||
|
'seedKeep': 'Keep seeding until manually stopped',
|
||||||
|
'seedRatio': 'Seed ratio',
|
||||||
|
'seedTime': 'Seed time (minutes)',
|
||||||
|
'setAsDefaultBtClient': 'Set as the default BT client',
|
||||||
|
'taskDetail': 'Detalhes da Tarefa',
|
||||||
|
'taskName': 'Nome da Tarefa',
|
||||||
|
'taskUrl': 'Tarefa URL',
|
||||||
|
'downloadPath': 'Download Path',
|
||||||
|
'skipVerifyCert': 'Pular Verificação de Certificado',
|
||||||
|
'name': 'Nome',
|
||||||
|
'size': 'Tamanho',
|
||||||
|
'unknown': 'Desconhecido',
|
||||||
|
'fileSelectedCount': 'Arquivos: ',
|
||||||
|
'fileSelectedSize': 'Tamanho: ',
|
||||||
|
'httpHeaderName': 'Nome do Header',
|
||||||
|
'httpHeaderValue': 'Valor do Header',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
const ruRU = {
|
const ruRU = {
|
||||||
'ru_RU': {
|
'ru_RU': {
|
||||||
'label': 'Русский(Россия)',
|
'label': 'Русский',
|
||||||
'error': 'Ошибка',
|
'error': 'Ошибка',
|
||||||
'tip': 'Подсказка',
|
'tip': 'Подсказка',
|
||||||
'confirm': 'Подтвердить',
|
'confirm': 'Подтвердить',
|
||||||
'cancel': 'Отмена',
|
'cancel': 'Отмена',
|
||||||
|
'on': 'Вкл.',
|
||||||
|
'off': 'Откл.',
|
||||||
'selectAll': 'Выбрать все',
|
'selectAll': 'Выбрать все',
|
||||||
|
'select': 'Выбрать',
|
||||||
'task': 'Задачи',
|
'task': 'Задачи',
|
||||||
'downloading': 'Загрузка',
|
'downloading': 'Загрузка',
|
||||||
'downloaded': 'Загружено',
|
'downloaded': 'Загружено',
|
||||||
@@ -13,7 +16,9 @@ const ruRU = {
|
|||||||
'donate': 'Пожертвовать',
|
'donate': 'Пожертвовать',
|
||||||
'exit': 'Выход',
|
'exit': 'Выход',
|
||||||
'create': 'Создать задачу',
|
'create': 'Создать задачу',
|
||||||
|
'directDownload': 'Прямая загрузка',
|
||||||
'advancedOptions': 'Расширенные опции',
|
'advancedOptions': 'Расширенные опции',
|
||||||
|
'followSettings': 'По умолчанию',
|
||||||
'downloadLink': 'Ссылка на скачивание',
|
'downloadLink': 'Ссылка на скачивание',
|
||||||
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
|
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
|
||||||
'downloadLinkHit':
|
'downloadLinkHit':
|
||||||
@@ -30,7 +35,9 @@ const ruRU = {
|
|||||||
'downloadDir': 'Папка загрузки',
|
'downloadDir': 'Папка загрузки',
|
||||||
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
|
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
|
||||||
'connections': 'Соединений',
|
'connections': 'Соединений',
|
||||||
|
'useServerCtime': 'Использовать время сервера для создания файла',
|
||||||
'maxRunning': 'Максимальное количество активных задач',
|
'maxRunning': 'Максимальное количество активных задач',
|
||||||
|
'defaultDirectDownload': 'Прямая загрузка по умолчанию',
|
||||||
'items': '@подсчет элементов',
|
'items': '@подсчет элементов',
|
||||||
'subscribeTracker': 'Подпись трекера',
|
'subscribeTracker': 'Подпись трекера',
|
||||||
'subscribeFail':
|
'subscribeFail':
|
||||||
@@ -40,11 +47,11 @@ const ruRU = {
|
|||||||
'lastUpdate': 'Последнее обновление: @время',
|
'lastUpdate': 'Последнее обновление: @время',
|
||||||
'addTracker': 'Добавить трекер',
|
'addTracker': 'Добавить трекер',
|
||||||
'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке',
|
'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке',
|
||||||
'ui': 'UI',
|
'ui': 'Интерфейс',
|
||||||
'theme': 'Тема',
|
'theme': 'Тема',
|
||||||
'themeSystem': 'Системная',
|
'themeSystem': 'Системная',
|
||||||
'themeLight': 'Светлая',
|
'themeLight': 'Светлая',
|
||||||
'themeDark': 'Темная',
|
'themeDark': 'Тёмная',
|
||||||
'locale': 'Язык',
|
'locale': 'Язык',
|
||||||
'about': 'О приложении',
|
'about': 'О приложении',
|
||||||
'homepage': 'Домашняя страница',
|
'homepage': 'Домашняя страница',
|
||||||
@@ -54,7 +61,13 @@ const ruRU = {
|
|||||||
'apiToken': 'API Токен',
|
'apiToken': 'API Токен',
|
||||||
'notSet': 'Не установлено',
|
'notSet': 'Не установлено',
|
||||||
'set': 'Установлено',
|
'set': 'Установлено',
|
||||||
|
'portInUse': 'Порт [@PORT] уже используется, пожалуйста, измените порт',
|
||||||
'effectAfterRestart': 'Эффект после перезагрузки',
|
'effectAfterRestart': 'Эффект после перезагрузки',
|
||||||
|
'developer': 'Разработчик',
|
||||||
|
'logDirectory': 'Каталог журналов',
|
||||||
|
'show': 'Показать',
|
||||||
|
'continue': 'Продолжить',
|
||||||
|
'pause': 'Пауза',
|
||||||
'startAll': 'Запустить все',
|
'startAll': 'Запустить все',
|
||||||
'pauseAll': 'Приостановить все',
|
'pauseAll': 'Приостановить все',
|
||||||
'deleteTask': 'Удалить @count задач',
|
'deleteTask': 'Удалить @count задач',
|
||||||
@@ -64,6 +77,7 @@ const ruRU = {
|
|||||||
'newVersionUpdate': 'Обновить',
|
'newVersionUpdate': 'Обновить',
|
||||||
'newVersionLater': 'позже',
|
'newVersionLater': 'позже',
|
||||||
'extensions': 'Расширения',
|
'extensions': 'Расширения',
|
||||||
|
'extensionInstallUrl': 'URL для установки',
|
||||||
'extensionInstallSuccess': 'Установка завершена',
|
'extensionInstallSuccess': 'Установка завершена',
|
||||||
'extensionUpdateSuccess': 'Обновление завершено',
|
'extensionUpdateSuccess': 'Обновление завершено',
|
||||||
'extensionDelete': 'Удалить расширение',
|
'extensionDelete': 'Удалить расширение',
|
||||||
@@ -73,10 +87,37 @@ const ruRU = {
|
|||||||
'history': 'История',
|
'history': 'История',
|
||||||
'clearHistory': 'Очистить историю',
|
'clearHistory': 'Очистить историю',
|
||||||
'noHistoryFound': 'История не найдена',
|
'noHistoryFound': 'История не найдена',
|
||||||
|
'serviceTitle': 'Служба загрузки',
|
||||||
|
'serviceText': 'Работает',
|
||||||
|
'network': 'Сеть',
|
||||||
|
'proxy': 'Прокси',
|
||||||
|
'noProxy': 'Без прокси',
|
||||||
|
'systemProxy': 'Системный прокси',
|
||||||
|
'customProxy': 'Пользовательский прокси',
|
||||||
|
'server': 'Сервер',
|
||||||
|
'username': 'Имя пользователя',
|
||||||
|
'password': 'Пароль',
|
||||||
'thanks': 'Благодарности',
|
'thanks': 'Благодарности',
|
||||||
'thanksDesc':
|
'thanksDesc':
|
||||||
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
|
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
|
||||||
'browserExtension': 'Расширение браузера',
|
'browserExtension': 'Расширение браузера',
|
||||||
|
'launchAtStartup': 'Запускать при загрузке системы',
|
||||||
|
'seedConfig': 'Настройка раздачи',
|
||||||
|
'seedKeep': 'Раздавать пока не остановлю вручную',
|
||||||
|
'seedRatio': 'Коэффициент раздачи',
|
||||||
|
'seedTime': 'Время раздачи (минуты)',
|
||||||
|
'setAsDefaultBtClient': 'Установить как клиент BT по умолчанию',
|
||||||
|
'taskDetail': 'Детали задания',
|
||||||
|
'taskName': 'Название задачи',
|
||||||
|
'taskUrl': 'URL задачи',
|
||||||
|
'downloadPath': 'Путь загрузки',
|
||||||
'skipVerifyCert': 'Пропустить проверку сертификата',
|
'skipVerifyCert': 'Пропустить проверку сертификата',
|
||||||
|
'name': 'Название',
|
||||||
|
'size': 'Размер',
|
||||||
|
'unknown': 'Неизвестно',
|
||||||
|
'fileSelectedCount': 'Файлов: ',
|
||||||
|
'fileSelectedSize': 'Размер: ',
|
||||||
|
'httpHeaderName': 'Название заголовка HTTP',
|
||||||
|
'httpHeaderValue': 'Значение заголовка HTTP',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -51,6 +51,7 @@ const zhCN = {
|
|||||||
'themeLight': '明亮主题',
|
'themeLight': '明亮主题',
|
||||||
'themeDark': '暗黑主题',
|
'themeDark': '暗黑主题',
|
||||||
'locale': '语言',
|
'locale': '语言',
|
||||||
|
'notifyWhenNewVersion': '有新版本时提醒',
|
||||||
'about': '关于',
|
'about': '关于',
|
||||||
'homepage': '主页',
|
'homepage': '主页',
|
||||||
'version': '版本',
|
'version': '版本',
|
||||||
@@ -116,5 +117,11 @@ const zhCN = {
|
|||||||
'fileSelectedSize': '大小:',
|
'fileSelectedSize': '大小:',
|
||||||
'httpHeaderName': '请求头名称',
|
'httpHeaderName': '请求头名称',
|
||||||
'httpHeaderValue': '请求头值',
|
'httpHeaderValue': '请求头值',
|
||||||
|
'login': '登录',
|
||||||
|
'username_required': '请输入用户名',
|
||||||
|
'password_required': '请输入密码',
|
||||||
|
'login_success': '登录成功',
|
||||||
|
'login_failed': '登录失败,请检查用户名和密码',
|
||||||
|
'login_failed_network': '登录失败,请检查网络连接',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import 'langs/de_de.dart';
|
||||||
import 'langs/en_us.dart';
|
import 'langs/en_us.dart';
|
||||||
import 'langs/fa_ir.dart';
|
import 'langs/fa_ir.dart';
|
||||||
import 'langs/fr_fr.dart';
|
import 'langs/fr_fr.dart';
|
||||||
@@ -16,6 +17,7 @@ import 'langs/zh_tw.dart';
|
|||||||
import 'langs/es_es.dart';
|
import 'langs/es_es.dart';
|
||||||
import 'langs/uk_ua.dart';
|
import 'langs/uk_ua.dart';
|
||||||
import 'langs/hu_hu.dart';
|
import 'langs/hu_hu.dart';
|
||||||
|
import 'langs/pt_br.dart';
|
||||||
|
|
||||||
final messages = _Messages();
|
final messages = _Messages();
|
||||||
|
|
||||||
@@ -39,5 +41,7 @@ class _Messages extends Translations {
|
|||||||
...esES,
|
...esES,
|
||||||
...ukUA,
|
...ukUA,
|
||||||
...huHU,
|
...huHU,
|
||||||
|
...ptBR,
|
||||||
|
...deDE
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,8 @@ class Args {
|
|||||||
|
|
||||||
bool hidden = false;
|
bool hidden = false;
|
||||||
|
|
||||||
|
Args();
|
||||||
|
|
||||||
Args.parse(List<String> args) {
|
Args.parse(List<String> args) {
|
||||||
final parser = ArgParser();
|
final parser = ArgParser();
|
||||||
parser.addFlag(flagHidden);
|
parser.addFlag(flagHidden);
|
||||||
@@ -33,7 +35,22 @@ class Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main(List<String> arguments) async {
|
void main(List<String> arguments) async {
|
||||||
final args = Args.parse(arguments);
|
Args args;
|
||||||
|
// Parse url scheme arguments, e.g. gopeed:?hidden=true
|
||||||
|
// TODO: macos open url handle
|
||||||
|
// TODO: macos updater test
|
||||||
|
if (arguments.firstOrNull?.startsWith("gopeed:") == true) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(arguments.first);
|
||||||
|
args = Args()..hidden = uri.queryParameters["hidden"] == "true";
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
args = Args.parse([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = Args.parse(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
await init(args);
|
await init(args);
|
||||||
onStart();
|
onStart();
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const _firefoxNativeHostsKey = r'Software\Mozilla\NativeMessagingHosts';
|
|||||||
|
|
||||||
/// Install host binary for browser extension
|
/// Install host binary for browser extension
|
||||||
Future<void> doInstallHost() async {
|
Future<void> doInstallHost() async {
|
||||||
final hostPath = Util.homePathJoin(_hostExecName);
|
final hostPath = await Util.homePathJoin(_hostExecName);
|
||||||
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
|
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
|
||||||
executable: true);
|
executable: true);
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ Future<bool> doCheckBrowserInstalled(Browser browser) async {
|
|||||||
Future<bool> doCheckManifestInstalled(Browser browser) async {
|
Future<bool> doCheckManifestInstalled(Browser browser) async {
|
||||||
if (await checkBrowserInstalled(browser) == false) return false;
|
if (await checkBrowserInstalled(browser) == false) return false;
|
||||||
|
|
||||||
final manifestPath = _getManifestPath(browser);
|
final manifestPath = await _getManifestPath(browser);
|
||||||
if (manifestPath == null) return false;
|
if (manifestPath == null) return false;
|
||||||
|
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
@@ -86,7 +86,7 @@ Future<void> doInstallManifest(Browser browser) async {
|
|||||||
if (await checkBrowserInstalled(browser) == false) return;
|
if (await checkBrowserInstalled(browser) == false) return;
|
||||||
if (await checkManifestInstalled(browser)) return;
|
if (await checkManifestInstalled(browser)) return;
|
||||||
|
|
||||||
final manifestPath = _getManifestPath(browser)!;
|
final manifestPath = (await _getManifestPath(browser))!;
|
||||||
final manifestContent = await _getManifestContent(browser);
|
final manifestContent = await _getManifestContent(browser);
|
||||||
final manifestDir = path.dirname(manifestPath);
|
final manifestDir = path.dirname(manifestPath);
|
||||||
await Directory(manifestDir).create(recursive: true);
|
await Directory(manifestDir).create(recursive: true);
|
||||||
@@ -209,11 +209,11 @@ List<String> _getUnixExecutablePaths(Browser browser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _getManifestPath(Browser browser) {
|
Future<String?> _getManifestPath(Browser browser) async {
|
||||||
final manifestName =
|
final manifestName =
|
||||||
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
|
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
return Util.homePathJoin(manifestName);
|
return await Util.homePathJoin(manifestName);
|
||||||
}
|
}
|
||||||
|
|
||||||
final home =
|
final home =
|
||||||
@@ -259,7 +259,7 @@ Future<bool> _checkWindowsRegistry(String keyPath) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getManifestContent(Browser browser) async {
|
Future<String> _getManifestContent(Browser browser) async {
|
||||||
final hostPath = Util.homePathJoin(_hostExecName);
|
final hostPath = await Util.homePathJoin(_hostExecName);
|
||||||
final manifest = {
|
final manifest = {
|
||||||
'name': _hostName,
|
'name': _hostName,
|
||||||
'description': 'Gopeed browser extension host',
|
'description': 'Gopeed browser extension host',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:open_dir/open_dir.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class FileExplorer {
|
class FileExplorer {
|
||||||
@@ -12,42 +13,19 @@ class FileExplorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _openDirectory(String directoryPath) async {
|
static Future<void> _openDirectory(String directoryPath) async {
|
||||||
await launchUrlString("file://$directoryPath");
|
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||||
|
await OpenDir().openNativeDir(path: directoryPath);
|
||||||
|
} else {
|
||||||
|
await launchUrlString("file://$directoryPath");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _openFile(String filePath) async {
|
static Future<void> _openFile(String filePath) async {
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
|
||||||
Process.run('explorer.exe', ['/select,', filePath]);
|
final fileName = path.basename(filePath);
|
||||||
} else if (Platform.isMacOS) {
|
final parentPath = path.dirname(filePath);
|
||||||
Process.run('open', ['-R', filePath]);
|
await OpenDir()
|
||||||
} else if (Platform.isLinux) {
|
.openNativeDir(path: parentPath, highlightedFileName: fileName);
|
||||||
_linuxOpen(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _linuxOpen(String filePath) async {
|
|
||||||
if (await Process.run('which', ['xdg-open'])
|
|
||||||
.then((value) => value.exitCode == 0)) {
|
|
||||||
final result = await Process.run('xdg-open', [filePath]);
|
|
||||||
if (result.exitCode != 0) {
|
|
||||||
_openWithFileManager(filePath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_openWithFileManager(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _openWithFileManager(String filePath) async {
|
|
||||||
final desktop = Platform.environment['XDG_CURRENT_DESKTOP'];
|
|
||||||
if (desktop == null) {
|
|
||||||
throw Exception('XDG_CURRENT_DESKTOP is not set');
|
|
||||||
}
|
|
||||||
if (desktop == 'GNOME') {
|
|
||||||
await Process.run('nautilus', ['--select', filePath]);
|
|
||||||
} else if (desktop == 'KDE') {
|
|
||||||
await Process.run('dolphin', ['--select', filePath]);
|
|
||||||
} else {
|
|
||||||
throw Exception('Unsupported desktop environment');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@ import 'package:dio/dio.dart';
|
|||||||
|
|
||||||
// List of mirrors for GitHub
|
// List of mirrors for GitHub
|
||||||
const _sourceMirror = [
|
const _sourceMirror = [
|
||||||
// ghfast.top: https://ghfast.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
// github.tbedu.top: https://github.tbedu.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
["https://ghfast.top/", r"(.*)"],
|
["https://github.tbedu.top/", r"(.*)"],
|
||||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
// fastgit.cc: https://fastgit.cc/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
["https://github.moeyy.xyz/", r"(.*)"],
|
["https://fastgit.cc/", r"(.*)"],
|
||||||
|
// gitproxy.click: https://gitproxy.click/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
|
["https://gitproxy.click/", r"(.*)"],
|
||||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||||
[
|
[
|
||||||
"https://fastly.jsdelivr.net/gh",
|
"https://fastly.jsdelivr.net/gh",
|
||||||
@@ -15,10 +17,12 @@ const _sourceMirror = [
|
|||||||
|
|
||||||
// List of mirrors for GitHub release
|
// List of mirrors for GitHub release
|
||||||
const _assertMirror = [
|
const _assertMirror = [
|
||||||
// ghfast.top: https://ghfast.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
// github.tbedu.top: https://https://github.tbedu.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||||
["https://ghfast.top/", r"(.*)"],
|
["https://github.tbedu.top/", r"(.*)"],
|
||||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
// fastgit.cc: https://fastgit.cc/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||||
["https://github.moeyy.xyz/", r"(.*)"],
|
["https://fastgit.cc/", r"(.*)"],
|
||||||
|
// gitproxy.click: https://gitproxy.click/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||||
|
["https://gitproxy.click/", r"(.*)"],
|
||||||
];
|
];
|
||||||
|
|
||||||
enum MirrorType {
|
enum MirrorType {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ final _updaterBin = "updater${Util.isWindows() ? ".exe" : ""}";
|
|||||||
|
|
||||||
Future<void> installUpdater() async {
|
Future<void> installUpdater() async {
|
||||||
await Util.installAsset(
|
await Util.installAsset(
|
||||||
'assets/exec/$_updaterBin', Util.homePathJoin(_updaterBin),
|
'assets/exec/$_updaterBin', await Util.homePathJoin(_updaterBin),
|
||||||
executable: true);
|
executable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +346,7 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
|
|||||||
case Channel.linuxFlathub:
|
case Channel.linuxFlathub:
|
||||||
case Channel.linuxSnap:
|
case Channel.linuxSnap:
|
||||||
case Channel.linuxDeb:
|
case Channel.linuxDeb:
|
||||||
final updaterPath = Util.homePathJoin(_updaterBin);
|
final updaterPath = await Util.homePathJoin(_updaterBin);
|
||||||
// Check the updater binary is exists
|
// Check the updater binary is exists
|
||||||
if (!await File(updaterPath).exists()) {
|
if (!await File(updaterPath).exists()) {
|
||||||
await launchUrl(
|
await launchUrl(
|
||||||
@@ -363,6 +363,8 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
|
|||||||
Update channel
|
Update channel
|
||||||
-asset string
|
-asset string
|
||||||
Path to the package asset
|
Path to the package asset
|
||||||
|
-exeDir string
|
||||||
|
Directory of the entry executable
|
||||||
-log string
|
-log string
|
||||||
Log file path
|
Log file path
|
||||||
*/
|
*/
|
||||||
@@ -373,6 +375,8 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
|
|||||||
_channel!.name,
|
_channel!.name,
|
||||||
"-asset",
|
"-asset",
|
||||||
newVersionAssetPath,
|
newVersionAssetPath,
|
||||||
|
"-exeDir",
|
||||||
|
path.dirname(Platform.resolvedExecutable),
|
||||||
"-log",
|
"-log",
|
||||||
path.join(logsDir(), "updater.log")
|
path.join(logsDir(), "updater.log")
|
||||||
]);
|
]);
|
||||||
@@ -406,7 +410,7 @@ String _getAssetName(String version) {
|
|||||||
case Channel.windowsPortable:
|
case Channel.windowsPortable:
|
||||||
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
|
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
|
||||||
case Channel.macosDmg:
|
case Channel.macosDmg:
|
||||||
return 'Gopeed-v$version-macos.dmg';
|
return 'Gopeed-v$version-macos-${commonArchName()}.dmg';
|
||||||
case Channel.linuxDeb:
|
case Channel.linuxDeb:
|
||||||
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
|
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
|
||||||
case Channel.androidApk:
|
case Channel.androidApk:
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ class Util {
|
|||||||
static Future<void> initStorageDir() async {
|
static Future<void> initStorageDir() async {
|
||||||
var storageDir = "";
|
var storageDir = "";
|
||||||
if (Util.isWindows()) {
|
if (Util.isWindows()) {
|
||||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
storageDir =
|
||||||
|
path.join(File(Platform.resolvedExecutable).parent.path, "storage");
|
||||||
} else if (!Util.isWeb()) {
|
} else if (!Util.isWeb()) {
|
||||||
if (Util.isLinux()) {
|
if (Util.isLinux()) {
|
||||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
storageDir = File(Platform.resolvedExecutable).parent.path;
|
||||||
@@ -156,10 +157,15 @@ class Util {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static String homePathJoin(String fileName) {
|
static Future<String> homePathJoin(String fileName) async {
|
||||||
final execPath = Platform.resolvedExecutable;
|
if (Util.isWindows()) {
|
||||||
final execDir = path.dirname(execPath);
|
final execPath = Platform.resolvedExecutable;
|
||||||
return path.join(execDir, fileName);
|
final execDir = path.dirname(execPath);
|
||||||
|
return path.join(execDir, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final dir = await getApplicationSupportDirectory();
|
||||||
|
return path.join(dir.path, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> installAsset(String assetPath, String targetPath,
|
static Future<void> installAsset(String assetPath, String targetPath,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ endif()
|
|||||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wno-error=deprecated-declarations) # Allow deprecated warnings
|
||||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|||||||
+141
-52
@@ -5,15 +5,15 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "76.0.0"
|
version: "72.0.0"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.3.3"
|
version: "0.3.2"
|
||||||
after_layout:
|
after_layout:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -26,10 +26,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.11.0"
|
version: "6.7.0"
|
||||||
animations:
|
animations:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -90,10 +90,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.11.0"
|
||||||
badges:
|
badges:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -106,10 +106,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.1"
|
||||||
build:
|
build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -178,10 +178,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.3.0"
|
||||||
checkable_treeview:
|
checkable_treeview:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -210,10 +210,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: clock
|
name: clock
|
||||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.1"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -226,10 +226,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.18.0"
|
||||||
contentsize_tabbarview:
|
contentsize_tabbarview:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -286,6 +286,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dart_ipc:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dart_ipc
|
||||||
|
sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -346,10 +354,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.1"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -448,6 +456,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.23"
|
version: "2.0.23"
|
||||||
|
flutter_svg:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_svg
|
||||||
|
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.17"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -607,18 +623,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "10.0.5"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.5"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -671,18 +687,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: macros
|
name: macros
|
||||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3-main.0"
|
version: "0.1.2-main.4"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.16+1"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -711,10 +727,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.15.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -723,6 +739,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
open_dir:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: open_dir
|
||||||
|
sha256: a4884b00e5e5795a9b4b3d582ac6a66e9196795ed760dbc3c63b4837c70c5901
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
|
open_dir_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_linux
|
||||||
|
sha256: "566cd9e02403971be06af35e1abc8057a4f3f98888c1226042e96a2af333b8bc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
|
open_dir_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_macos
|
||||||
|
sha256: "51fdc8c3a06c9d571b599b5901045ada23d1440b24c3052c0a66cf3ee4ac901b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
open_dir_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_platform_interface
|
||||||
|
sha256: ca189abb02d8e3320f9b2493b6d58e3a33f393d5eb4ccbbef02e0bc0fd393872
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
open_dir_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: open_dir_windows
|
||||||
|
sha256: ec48df32ce61adb6f6cede0330d13b0d89714d2ee2df198f32ecd520e3a5d250
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
open_filex:
|
open_filex:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -759,10 +815,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.0"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1088,7 +1152,7 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.99"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1109,10 +1173,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.0"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1125,18 +1189,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
version: "1.11.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.2"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1149,10 +1213,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.2.0"
|
||||||
styled_widget:
|
styled_widget:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1165,18 +1229,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.1"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.2"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1196,11 +1260,12 @@ packages:
|
|||||||
tray_manager:
|
tray_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: tray_manager
|
path: "packages/tray_manager"
|
||||||
sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7
|
ref: main
|
||||||
url: "https://pub.dev"
|
resolved-ref: "91cdbdc12517d0d832aca715542b9d0329739d5d"
|
||||||
source: hosted
|
url: "https://github.com/monkeyWie/tray_manager.git"
|
||||||
version: "0.2.4"
|
source: git
|
||||||
|
version: "0.5.1"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1289,6 +1354,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.1"
|
version: "4.5.1"
|
||||||
|
vector_graphics:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics
|
||||||
|
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.18"
|
||||||
|
vector_graphics_codec:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_codec
|
||||||
|
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.13"
|
||||||
|
vector_graphics_compiler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_graphics_compiler
|
||||||
|
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.16"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1301,10 +1390,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.1"
|
version: "14.2.5"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1333,10 +1422,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.3"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1394,5 +1483,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0-0 <4.0.0"
|
dart: ">=3.5.0 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.24.0"
|
||||||
+10
-2
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.6.11+1
|
version: 1.8.2+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
@@ -58,7 +58,11 @@ dependencies:
|
|||||||
form_builder_validators: ^11.0.0
|
form_builder_validators: ^11.0.0
|
||||||
flutter_foreground_task: ^8.2.0
|
flutter_foreground_task: ^8.2.0
|
||||||
open_filex: ^4.5.0
|
open_filex: ^4.5.0
|
||||||
tray_manager: ^0.2.4
|
tray_manager:
|
||||||
|
git:
|
||||||
|
url: https://github.com/monkeyWie/tray_manager.git
|
||||||
|
ref: main
|
||||||
|
path: packages/tray_manager
|
||||||
lecle_downloads_path_provider: ^0.0.2+8
|
lecle_downloads_path_provider: ^0.0.2+8
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
launch_at_startup: ^0.3.1
|
launch_at_startup: ^0.3.1
|
||||||
@@ -72,10 +76,13 @@ dependencies:
|
|||||||
win32_registry: ^1.1.5
|
win32_registry: ^1.1.5
|
||||||
share_handler: ^0.0.22
|
share_handler: ^0.0.22
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
|
open_dir: ^0.0.2+1
|
||||||
install_plugin:
|
install_plugin:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/hui-z/flutter_install_plugin.git
|
url: https://github.com/hui-z/flutter_install_plugin.git
|
||||||
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||||
|
flutter_svg: ^2.0.17
|
||||||
|
dart_ipc: ^1.0.1
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
git:
|
git:
|
||||||
@@ -112,6 +119,7 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/tray_icon/
|
- assets/tray_icon/
|
||||||
- assets/extension/
|
- assets/extension/
|
||||||
|
- assets/icon/
|
||||||
# Browser extension native host binary, see https://github.com/GopeedLab/gopeed/tree/main/cmd/host
|
# Browser extension native host binary, see https://github.com/GopeedLab/gopeed/tree/main/cmd/host
|
||||||
- assets/exec/
|
- assets/exec/
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!--
|
|
||||||
If you are serving your web app in a path other than the root, change the
|
|
||||||
href value below to reflect the base path you are serving from.
|
|
||||||
|
|
||||||
The path provided below has to start and end with a slash "/" in order for
|
|
||||||
it to work correctly.
|
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
||||||
|
|
||||||
This is a placeholder for base href that will be replaced by the value of
|
|
||||||
the `--base-href` argument provided to `flutter build`.
|
|
||||||
-->
|
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A modern download manager.">
|
<meta name="description" content="A modern download manager.">
|
||||||
|
|||||||
-169
@@ -1,169 +0,0 @@
|
|||||||
# Gopeed Flutter V2 UI - AI Coding Instructions
|
|
||||||
|
|
||||||
## Response Rules
|
|
||||||
|
|
||||||
Respond in the same language as user's question
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
This is the Flutter V2 UI for **Gopeed** (Go Speed), a high-speed download manager built with Golang + Flutter. This Flutter app is a **desktop-focused UI redesign** with cross-platform support (macOS, Windows, Linux, iOS, Android, Web).
|
|
||||||
|
|
||||||
**Critical Context**: This Flutter UI communicates with a Go backend REST API (not in this workspace). The backend handles download operations, task management, and file I/O. The UI is purely presentational with future API integration planned.
|
|
||||||
|
|
||||||
## Architecture & Structure
|
|
||||||
|
|
||||||
### Module Organization (Feature-First)
|
|
||||||
|
|
||||||
```
|
|
||||||
lib/
|
|
||||||
├── main.dart # App entry, window config, router setup
|
|
||||||
├── routes/ # Centralized routing
|
|
||||||
│ ├── app_routes.dart # go_router configuration
|
|
||||||
│ └── route_names.dart # Route path constants
|
|
||||||
├── modules/ # Feature modules (screens + widgets)
|
|
||||||
│ ├── home/ # Shell layout with sidebar
|
|
||||||
│ ├── task/ # Download task management UI
|
|
||||||
│ └── settings/ # Settings screen
|
|
||||||
└── components/ # Shared UI components (gp_* prefix)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Pattern**: Features are organized in `modules/` with `screens/` and `widgets/` subdirectories. Shared components use `gp_` prefix (e.g., `GpGradientButton`, `GpIconButton`).
|
|
||||||
|
|
||||||
### Routing Architecture
|
|
||||||
|
|
||||||
- Uses `go_router` with `ShellRoute` for persistent sidebar navigation
|
|
||||||
- `HomeScreen` wraps all routes and provides the sidebar layout
|
|
||||||
- Route paths defined in `RouteNames` class (prevents magic strings)
|
|
||||||
- Current route detection via `GoRouterState.of(context).matchedLocation`
|
|
||||||
|
|
||||||
## Critical Patterns & Conventions
|
|
||||||
|
|
||||||
### Component Design System
|
|
||||||
|
|
||||||
1. **Gradient Buttons** (`GpGradientButton`): Primary actions, teal-to-green gradient (Colors: `0xFF3ACBBE` → `0xFF2CDB90`)
|
|
||||||
2. **Outline Buttons** (`GpOutlineButton`): Secondary actions, border-only with teal accent (`0xFF39CDB9`)
|
|
||||||
3. **Icon Buttons** (`GpIconButton`): Tertiary actions with hover effects (`0xFF4883A2` on hover)
|
|
||||||
|
|
||||||
### Design Implementation
|
|
||||||
|
|
||||||
- **Glass Morphism**: `TaskItem` widget uses `BackdropFilter` with blur (24px) and layered gradients
|
|
||||||
- **Color Palette**: Dark blue theme (`0xFF131956` background, `0xFF000E4B` sidebar, teal/green accents)
|
|
||||||
- **Comments Reference Design Specs**: Many widgets have detailed comments like "Rectangle 82: 主容器" mapping to Figma/design files
|
|
||||||
|
|
||||||
### Widget Documentation Style
|
|
||||||
|
|
||||||
Components use triple-slash (`///`) documentation:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
/// TaskItem - 任务项组件
|
|
||||||
///
|
|
||||||
/// 一个带有毛玻璃效果和渐变边框的任务项容器
|
|
||||||
class TaskItem extends StatelessWidget {
|
|
||||||
/// 左侧图标
|
|
||||||
final dynamic icon;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Convention**: Document widget purpose, parameters, and design intent in Chinese. Keep inline comments for implementation details.
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
|
|
||||||
- Currently uses `StatefulWidget` with local state (e.g., `TaskScreen._selectedTabIndex`)
|
|
||||||
- **Future Integration**: Will integrate with backend API - prepare for state management library (GetX/Riverpod likely based on Gopeed ecosystem)
|
|
||||||
|
|
||||||
### Window Management
|
|
||||||
|
|
||||||
Desktop apps use `window_manager` package:
|
|
||||||
|
|
||||||
- Initial size: 1024x768, minimum: 800x600
|
|
||||||
- Hidden title bar (`TitleBarStyle.hidden`) for custom UI
|
|
||||||
- Configured in `main.dart` before `runApp()`
|
|
||||||
|
|
||||||
## Development Workflows
|
|
||||||
|
|
||||||
### Running the App
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Desktop (macOS/Windows/Linux)
|
|
||||||
flutter run -d macos # or windows/linux
|
|
||||||
|
|
||||||
# Mobile
|
|
||||||
flutter run -d ios # requires Xcode
|
|
||||||
flutter run -d android
|
|
||||||
|
|
||||||
# Web
|
|
||||||
flutter run -d chrome
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
flutter build macos --release
|
|
||||||
flutter build windows --release
|
|
||||||
flutter build linux --release
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
Key packages in `pubspec.yaml`:
|
|
||||||
|
|
||||||
- `window_manager: ^0.4.3` - Desktop window control
|
|
||||||
- `flutter_svg: ^2.0.17` - SVG asset support
|
|
||||||
- `go_router: ^14.8.1` - Declarative routing
|
|
||||||
|
|
||||||
Assets in `assets/icons/` (e.g., `gopeed_sidebar.svg`, `pause.png`)
|
|
||||||
|
|
||||||
## Backend Integration (Future)
|
|
||||||
|
|
||||||
**Not Yet Implemented**: Based on parent project structure, this UI will connect to Go REST API:
|
|
||||||
|
|
||||||
- API endpoints: `/api/task`, `/api/resolve`, `/api/config` (reference: `pkg/rest/api.go` in parent)
|
|
||||||
- Task operations: create, pause, continue, delete
|
|
||||||
- Real-time progress updates via events/WebSocket
|
|
||||||
|
|
||||||
**When Implementing API**:
|
|
||||||
|
|
||||||
1. Create `lib/api/` or `lib/services/` directory
|
|
||||||
2. Use `http` or `dio` package for REST calls
|
|
||||||
3. Models should match Go backend structs (e.g., `Task`, `Progress`)
|
|
||||||
4. Replace mock data in `TaskScreen._buildTaskList()` with API calls
|
|
||||||
|
|
||||||
## Testing & Quality
|
|
||||||
|
|
||||||
- Lints: Uses `package:flutter_lints/flutter.yaml` (standard Flutter best practices)
|
|
||||||
- No tests currently - **add tests when backend integration begins**
|
|
||||||
- Desktop focus: Prioritize macOS/Windows, mobile is secondary
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
### Adding a New Screen
|
|
||||||
|
|
||||||
1. Create `lib/modules/[feature]/screens/[screen_name].dart`
|
|
||||||
2. Add route to `RouteNames` constant
|
|
||||||
3. Register route in `AppRoutes.getRouter()` under `ShellRoute.routes`
|
|
||||||
|
|
||||||
### Adding Shared Component
|
|
||||||
|
|
||||||
1. Create `lib/components/gp_[component_name].dart`
|
|
||||||
2. Follow naming: `Gp` prefix + PascalCase
|
|
||||||
3. Document with `///` comments
|
|
||||||
4. Support flexible sizing/theming via parameters
|
|
||||||
|
|
||||||
### Design-to-Code Workflow
|
|
||||||
|
|
||||||
When implementing from design files:
|
|
||||||
|
|
||||||
- Preserve design layer names in comments (e.g., "Rectangle 82")
|
|
||||||
- Use exact hex colors from specs
|
|
||||||
- Document gradient stops and blur sigma values
|
|
||||||
- Stack layers match design hierarchy (bottom-to-top)
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- **Language**: UI text is in Chinese (e.g., "下载中", "已完成", "任务失败")
|
|
||||||
- **Branch Context**: Current branch is `feat/new_ui` - this is a redesign
|
|
||||||
- **Desktop First**: Window management, mouse hover states prioritized
|
|
||||||
- **Parent Project**: Part of larger Gopeed monorepo with Go backend (outside this workspace)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**When uncertain**: Check `lib/modules/task/widgets/task_item.dart` for complex widget patterns, `lib/routes/app_routes.dart` for routing examples, or `lib/main.dart` for app initialization.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Miscellaneous
|
|
||||||
*.class
|
|
||||||
*.log
|
|
||||||
*.pyc
|
|
||||||
*.swp
|
|
||||||
.DS_Store
|
|
||||||
.atom/
|
|
||||||
.build/
|
|
||||||
.buildlog/
|
|
||||||
.history
|
|
||||||
.svn/
|
|
||||||
.swiftpm/
|
|
||||||
migrate_working_dir/
|
|
||||||
|
|
||||||
# IntelliJ related
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
|
||||||
# is commented out by default.
|
|
||||||
#.vscode/
|
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
|
||||||
**/doc/api/
|
|
||||||
**/ios/Flutter/.last_build_id
|
|
||||||
.dart_tool/
|
|
||||||
.flutter-plugins
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
.pub-cache/
|
|
||||||
.pub/
|
|
||||||
/build/
|
|
||||||
|
|
||||||
# Symbolication related
|
|
||||||
app.*.symbols
|
|
||||||
|
|
||||||
# Obfuscation related
|
|
||||||
app.*.map.json
|
|
||||||
|
|
||||||
# Android Studio will place build artifacts here
|
|
||||||
/android/app/debug
|
|
||||||
/android/app/profile
|
|
||||||
/android/app/release
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
|
||||||
#
|
|
||||||
# This file should be version controlled and should not be manually edited.
|
|
||||||
|
|
||||||
version:
|
|
||||||
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
|
|
||||||
channel: "stable"
|
|
||||||
|
|
||||||
project_type: app
|
|
||||||
|
|
||||||
# Tracks metadata for the flutter migrate command
|
|
||||||
migration:
|
|
||||||
platforms:
|
|
||||||
- platform: root
|
|
||||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
- platform: android
|
|
||||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
- platform: ios
|
|
||||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
- platform: linux
|
|
||||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
- platform: macos
|
|
||||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
- platform: web
|
|
||||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
- platform: windows
|
|
||||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
|
||||||
|
|
||||||
# User provided section
|
|
||||||
|
|
||||||
# List of Local paths (relative to this file) that should be
|
|
||||||
# ignored by the migrate tool.
|
|
||||||
#
|
|
||||||
# Files that are not part of the templates will be ignored by default.
|
|
||||||
unmanaged_files:
|
|
||||||
- 'lib/main.dart'
|
|
||||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
|
||||||
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"dart.mcpServer": true
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# gopeed
|
|
||||||
|
|
||||||
A new Flutter project.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
|
||||||
|
|
||||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
|
||||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
|
||||||
# check for errors, warnings, and lints.
|
|
||||||
#
|
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
|
||||||
|
|
||||||
linter:
|
|
||||||
# The lint rules applied to this project can be customized in the
|
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
||||||
# included above or to enable additional rules. A list of all available lints
|
|
||||||
# and their documentation is published at https://dart.dev/lints.
|
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
gradle-wrapper.jar
|
|
||||||
/.gradle
|
|
||||||
/captures/
|
|
||||||
/gradlew
|
|
||||||
/gradlew.bat
|
|
||||||
/local.properties
|
|
||||||
GeneratedPluginRegistrant.java
|
|
||||||
.cxx/
|
|
||||||
|
|
||||||
# Remember to never publicly share your keystore.
|
|
||||||
# See https://flutter.dev/to/reference-keystore
|
|
||||||
key.properties
|
|
||||||
**/*.keystore
|
|
||||||
**/*.jks
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.android.application")
|
|
||||||
id("kotlin-android")
|
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.gopeed.gopeed"
|
|
||||||
compileSdk = flutter.compileSdkVersion
|
|
||||||
ndkVersion = flutter.ndkVersion
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId = "com.gopeed.gopeed"
|
|
||||||
// You can update the following values to match your application needs.
|
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
|
||||||
minSdk = flutter.minSdkVersion
|
|
||||||
targetSdk = flutter.targetSdkVersion
|
|
||||||
versionCode = flutter.versionCode
|
|
||||||
versionName = flutter.versionName
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
// TODO: Add your own signing config for the release build.
|
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flutter {
|
|
||||||
source = "../.."
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
|
||||||
the Flutter tool needs it to communicate with the running application
|
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
|
||||||
-->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application
|
|
||||||
android:label="gopeed"
|
|
||||||
android:name="${applicationName}"
|
|
||||||
android:icon="@mipmap/ic_launcher">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:taskAffinity=""
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
|
||||||
the Android process has started. This theme is visible to the user
|
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
|
||||||
to determine the Window background behind the Flutter UI. -->
|
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
|
||||||
android:resource="@style/NormalTheme"
|
|
||||||
/>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<!-- Don't delete the meta-data below.
|
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2" />
|
|
||||||
</application>
|
|
||||||
<!-- Required to query activities that can process text, see:
|
|
||||||
https://developer.android.com/training/package-visibility and
|
|
||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
|
||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
|
||||||
<queries>
|
|
||||||
<intent>
|
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
|
||||||
<data android:mimeType="text/plain"/>
|
|
||||||
</intent>
|
|
||||||
</queries>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.gopeed.gopeed
|
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="?android:colorBackground" />
|
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="@android:color/white" />
|
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 544 B |
Binary file not shown.
|
Before Width: | Height: | Size: 442 B |
Binary file not shown.
|
Before Width: | Height: | Size: 721 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
Loaded 100 of 237 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user