Compare commits

...
72 Commits
Author SHA1 Message Date
Levi b329fee107 release: v1.6.6 2024-12-27 19:11:18 +08:00
Levi 3ce95f21fd fix: some torrents can not resovle (#857) 2024-12-27 19:06:17 +08:00
Levi f54ed5df5a feat: batch operation task (#860) 2024-12-27 18:52:23 +08:00
Levi 727b7edb84 feat: file tree view responsive support (#856) 2024-12-27 11:34:56 +08:00
Levi f5891ee03d feat: add batch create tips (#858) 2024-12-27 11:17:17 +08:00
Levi 724276ac69 release: v1.6.5 2024-12-25 22:56:09 +08:00
Levi a3f559c8e5 feat: improve file select tree view (#853) 2024-12-25 22:55:33 +08:00
Levi edd189a8a8 fix: failed to delete a running task (#852) 2024-12-24 22:23:20 +08:00
Levi c970378fd7 fix: http download retries too many times (#849) 2024-12-24 20:28:04 +08:00
Levi 8b61343aef fix: extension task missing fields (#850) 2024-12-23 01:58:29 +08:00
Levi 2cb93a03e2 perf: extension onStart hook will block task create (#844) 2024-12-21 09:58:33 +08:00
Levi 81dfc073a6 release: v1.6.4 2024-12-09 20:30:33 +08:00
Levi 427921ccab perf: write to disk only when progress updated (#839) 2024-12-09 20:29:50 +08:00
Levi f4876c8a0e fix: HTTP fetch crash when modified request extra via extension (#832) 2024-12-08 12:07:08 +08:00
Navy.Wang 35a3625b11 feat: add QNAP platform build (#828) 2024-11-29 13:38:17 +08:00
ilkei dc9b501e35 feat: optimize docker build (#827) 2024-11-27 13:40:28 +08:00
Levi 85fe92a378 release: v1.6.3 2024-11-18 18:54:46 +08:00
Levi 983d9ad09e fix: windows location fix patch not effect (#819) 2024-11-18 18:54:15 +08:00
Levi d9f757aaf0 fix: revert flutter version to v3.16.8 (#821) 2024-11-18 18:31:25 +08:00
Levi cd409fcfb9 ci: add build tests for all platforms (#812) 2024-11-15 12:06:51 +08:00
Levi 366244fa5d fix: android build on go 1.23 (#813) 2024-11-15 11:43:55 +08:00
Levi 7a2e9b4fd1 release: v1.6.2 2024-11-14 23:45:08 +08:00
Levi 203385d3f8 fix: use golang 1.23 standard library to collect crash logs (#807) 2024-11-14 00:54:34 +08:00
Levi aaf73dec24 fix: proxy config reload panic (#808) 2024-11-14 00:36:21 +08:00
Ricky 2523282a89 feat(http): add option to skip SSL certificate verification (#801) 2024-11-12 10:30:21 +08:00
Levi 3384f5197a fix: download path in docker-compose.yml (#798) 2024-11-05 09:55:25 +08:00
Levi cd77665f02 fix: avoid request locatoin permisson on windows (#787) 2024-10-19 16:55:23 +08:00
Levi a22b431150 feat: upgrade flutter to v3.24.3 (#791) 2024-10-19 12:12:41 +08:00
Levi eb7c9ad7f2 release: v1.6.1 2024-10-16 18:31:56 +08:00
Levi ea11fc38f1 ci: fix windows build version (#785) 2024-10-16 18:30:41 +08:00
towermom9 1be16e9f2c feat: Updated the indonesian translation (#777) 2024-10-10 13:18:11 +08:00
Ayase 1d86d14a20 fix: open files on linux (#765) 2024-10-03 21:40:49 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣 5ad15cc4ac feat: Update tr_tr.dart (#754) 2024-09-23 09:19:45 +08:00
Levi 49604966fd release: v1.6.0 2024-09-21 11:00:47 +08:00
Levi bdb8ed177f feat: supports batch operation of tasks by filter (#753) 2024-09-21 10:19:52 +08:00
Levi 2dc7048cba feat: support download directory white list (#752) 2024-09-20 21:32:21 +08:00
Levi d3750464aa fix: torrent stats panic (#737) 2024-09-20 12:28:24 +08:00
Levi 5c9bcd3731 fix: DotAll flag not working with group (#750) 2024-09-19 21:28:46 +08:00
Aman Raj Singh Mourya bdda550b2c feat: avoid using the depreciated background (#746) 2024-09-18 14:39:01 +08:00
liwei 3308d966f0 docs: fix banner transparent 2024-09-13 18:40:58 +08:00
Levi 18041ed9ec docs: improve banner image (#740) 2024-09-11 19:02:39 +08:00
Levi 56b85e545e release: v1.5.9 2024-09-07 19:20:12 +08:00
Levi c5af916952 feat: support default direct download config (#735) 2024-09-07 14:08:17 +08:00
Levi a1981328aa fix: avoid HTTP zero connections (#729) 2024-09-07 13:49:32 +08:00
Levi 51244133e0 feat: support extension onDone event (#728) 2024-09-07 10:10:39 +08:00
Levi 0c17093b1a feat: task separate proxy (#727) 2024-09-06 18:35:23 +08:00
Levi 6538b29b62 feat: macos round icon (#719) 2024-09-03 18:47:49 +08:00
Levi 81e90e6b89 fix: try request grant external storage on Android (#660) 2024-09-03 18:47:06 +08:00
JuanQ 4f39296664 feat: Add spanish language (#667) 2024-08-27 08:59:18 +08:00
Levi ece5b91dc2 fix: torrent redundant directory (#649) 2024-08-14 21:54:59 +08:00
Levi d9f04ae928 fix: tasks block when paused while not ready (#648) 2024-08-14 21:22:23 +08:00
Levi e3ccecbd9c fix: task directory open (#647) 2024-08-14 21:21:41 +08:00
Levi fb1c2cb060 fix: downgrade minSdkVersion (#642) 2024-08-14 21:21:09 +08:00
Levi 9bff75464d ci: docker flutter build on host (#637) 2024-08-09 18:28:44 +08:00
Levi 863f759248 release: v1.5.8 2024-08-09 12:57:11 +08:00
Levi f89fb5e438 fix: web config file not loaded when is relative path (#636) 2024-08-09 12:50:20 +08:00
Levi 7ca1cc8c64 fix: batch create tasks with duplicate names (#635) 2024-08-09 12:41:03 +08:00
Levi 1f791c66e6 perf: excessive torrent tracker dns resolve (#634) 2024-08-08 18:55:52 +08:00
Levi 4d60c47d39 feat: task detail view (#633) 2024-08-08 18:34:00 +08:00
Levi 3fd3dd49d2 feat(extension): get exception stacks on promise error (#630) 2024-08-07 12:04:45 +08:00
foxxorcat d119de7b98 feat: enhanced extension engine (#629) 2024-08-05 10:44:17 +08:00
madoka773 69b359a9c9 fix: tray icon not shown on linux flatpak or snap (#625) 2024-08-02 12:36:25 +08:00
Levi 4fbe13a1f0 fix: clear UPNP port mapping when exist app (#624) 2024-07-30 18:43:54 +08:00
Levi 3b50465708 feat: torrent seed config (#599) 2024-07-30 12:50:06 +08:00
Levi 473c58796c fix: handle empty string params (#620) 2024-07-24 11:51:31 +08:00
abc0922001 fab8f135f3 feat: Update Traditional Chinese translation (#611) 2024-07-17 18:42:42 +08:00
liwei 0fca7b6bc7 fix: fr_fr import 2024-07-15 09:36:36 +08:00
torik988 4700d0ba28 feat: Create fr_fr.dart (#608) 2024-07-15 09:28:03 +08:00
towermom9 6e31ea089f feat: Indonesian Translation/Localization (#606) 2024-07-15 09:21:49 +08:00
Levi 4a2088f6fb fix: xhr string request body can specify Content-Type (#596) 2024-07-09 18:40:02 +08:00
𝗪𝗜𝗡𝗭𝗢𝗥𝗧 17c8fb7e4d feat: update tr_tr.dart (#593) 2024-07-03 09:52:01 +08:00
Levi 8c99bbc847 ci: create CODEOWNERS 2024-06-26 17:17:27 +08:00
130 changed files with 6368 additions and 4305 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
.git
*.data
*.log
node_modules
**/node_modules
Dockerfile
.dockerignore
.github
_docs
_examples
bin
ui
+1
View File
@@ -0,0 +1 @@
* @monkeyWie
+3
View File
@@ -11,6 +11,9 @@ categories:
- 'fix' - 'fix'
- 'bugfix' - 'bugfix'
- 'bug' - 'bug'
- title: '🔧 Performance Improvements'
labels:
- 'perf'
- title: '🧪 Tests' - title: '🧪 Tests'
label: 'test' label: 'test'
- title: '🧰 Maintenance' - title: '🧰 Maintenance'
+48 -4
View File
@@ -13,7 +13,7 @@ on:
default: "false" default: "false"
env: env:
GO_VERSION: "1.21" GO_VERSION: "1.23"
FLUTTER_VERSION: "3.16.8" FLUTTER_VERSION: "3.16.8"
permissions: permissions:
@@ -51,7 +51,7 @@ jobs:
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl" Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\" mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
$TAG = "v$env:VERSION" $TAG = "v$env:VERSION"
flutter build windows flutter build windows
@@ -380,7 +380,7 @@ jobs:
go install golang.org/x/mobile/cmd/gomobile@latest go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind go get golang.org/x/mobile/bind
gomobile init gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
flutter build apk flutter build apk
@@ -441,6 +441,7 @@ jobs:
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --web-renderer html
cd ../../ cd ../../
@@ -468,6 +469,39 @@ jobs:
|| true || true
done done
done done
sudo apt update -y
sudo apt install -y pv python2 bsdmainutils
wget -O qdk2_0.32.bionic_amd64.deb "https://github.com/qnap-dev/qdk2/releases/download/v0.32/qdk2_0.32.bionic_amd64.deb"
dpkg -X qdk2_0.32.bionic_amd64.deb qdk2 # Direct installs will fail due to missing dependencies!
[[ -d qdk2 ]] || exit 1
export PATH=$(pwd)/qdk2/usr/bin:$(pwd)/qdk2/usr/share/qdk2/QDK/bin:${PATH}
wget -O Gopeed.template.tar.gz "https://github.com/GopeedLab/QpkgBuild/raw/refs/heads/master/template/Gopeed.template.tar.gz"
tar -zxf Gopeed.template.tar.gz
[[ -d Gopeed ]] || exit 1
goos=linux
goarch_arr=(amd64 arm64)
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
name=gopeed-web-v${VERSION}-${goos}-${goarch}
cp dist/${name}/${name}/* Gopeed/${qarch}/
done
cd Gopeed
sed -i -e 's/__QPKG_VER__/${VERSION}/g' qpkg.cfg
qbuild || exit 1
for f in build/*; do echo ">>> build=[${f}]"; done
goos=qnap
goarch_arr=(amd64 arm64)
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
sname=Gopeed_${VERSION}_${qarch}.qpkg
dname=gopeed-${goos}-v${VERSION}-${goarch}.qpkg
echo "sname=${sname}, dname=${dname}"
[[ -f build/${sname} ]] && cp -ra build/${sname} ../dist/zip/${dname}
done
for f in ../dist/zip/*; do echo ">>> dist=[${f}]"; done
cd ..
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -483,6 +517,9 @@ jobs:
run: | run: |
sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/share/dotnet
sudo rm -rf "$AGENT_TOOLSDIRECTORY" sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
@@ -493,11 +530,19 @@ jobs:
with: with:
username: liwei2633 username: liwei2633
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build flutter web
run: |
cd ui/flutter
flutter build web --web-renderer html
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
with: with:
context: .
push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }} push: ${{ github.event.inputs.test == 'true' && 'false' || 'true' }}
build-args: | build-args: |
VERSION=${{ env.VERSION }} VERSION=${{ env.VERSION }}
@@ -507,7 +552,6 @@ jobs:
linux/amd64 linux/amd64
linux/arm64 linux/arm64
linux/arm/v7 linux/arm/v7
linux/arm64/v8
tags: | tags: |
liwei2633/gopeed:latest liwei2633/gopeed:latest
liwei2633/gopeed:v${{ env.VERSION }} liwei2633/gopeed:v${{ env.VERSION }}
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile" - "Dockerfile"
env: env:
GO_VERSION: "1.19" GO_VERSION: "1.23"
permissions: permissions:
contents: write contents: write
+98 -11
View File
@@ -27,7 +27,7 @@ on:
- "go.sum" - "go.sum"
env: env:
GO_VERSION: "1.21" GO_VERSION: "1.23"
FLUTTER_VERSION: "3.16.8" FLUTTER_VERSION: "3.16.8"
jobs: jobs:
@@ -54,14 +54,49 @@ jobs:
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: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v4
with: with:
files: ./coverage.txt files: ./coverage.txt
build: token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
name: Build check build-desktop:
runs-on: ubuntu-latest name: Build desktop check
# needs: [lint, test] runs-on: ${{ matrix.os }}
needs: [test] strategy:
matrix:
os: [ windows-latest, macos-latest, ubuntu-latest ]
needs: [ test ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- if: runner.os == 'windows'
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
- if: runner.os == 'macOS'
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
- if: runner.os == 'Linux'
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
build-mobile:
name: Build mobile check
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-latest, ubuntu-latest ]
needs: [ test ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -71,10 +106,62 @@ jobs:
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- run: | - run: |
sudo apt-get update -y go install golang.org/x/mobile/cmd/gomobile@latest
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev go get golang.org/x/mobile/bind
gomobile init
- if: runner.os == 'macOS'
run: |
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ipa --no-codesign
- if: runner.os == 'Linux'
uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "17"
- if: runner.os == 'Linux'
run: |
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
build-web:
name: Build web check
runs-on: ubuntu-latest
needs: [ test ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build - name: Build
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build linux flutter build web --web-renderer html
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" github.com/GopeedLab/gopeed/cmd/web
build-docker:
name: Build docker check
runs-on: ubuntu-latest
needs: [ test ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
run: |
cd ui/flutter
flutter build web --web-renderer html
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
docker build -t gopeed .
+3 -12
View File
@@ -1,29 +1,20 @@
FROM instrumentisto/flutter:3.16.8 AS flutter FROM golang:1.23.3 AS go
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
COPY ./ui/flutter ./
RUN flutter build web --web-renderer html
FROM golang:1.21.6 AS go
WORKDIR /app WORKDIR /app
COPY ./go.mod ./go.sum ./ COPY ./go.mod ./go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
COPY --from=flutter /app/build/web ./cmd/web/dist
ARG VERSION=dev ARG VERSION=dev
RUN CGO_ENABLED=0 go build -tags nosqlite,web \ RUN CGO_ENABLED=0 go build -tags nosqlite,web \
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \ -ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
FROM alpine:3.14.2 FROM alpine:3.18
LABEL maintainer="monkeyWie" LABEL maintainer="monkeyWie"
WORKDIR /app WORKDIR /app
COPY --from=go /app/dist/gopeed ./ COPY --from=go /app/dist/gopeed ./
COPY entrypoint.sh ./entrypoint.sh COPY entrypoint.sh ./entrypoint.sh
RUN apk update && \ RUN apk update && \
apk upgrade --no-cache && \ apk add --no-cache su-exec ; \
apk add --no-cache bash su-exec; \
chmod +x ./entrypoint.sh && \ chmod +x ./entrypoint.sh && \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
VOLUME ["/app/storage"] VOLUME ["/app/storage"]
+5 -3
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -112,7 +114,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### Environment ### Environment
1. Golang 1.21+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.16+
### Clone ### Clone
@@ -172,7 +174,7 @@ command:
- android - android
```bash ```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build apk flutter build apk
``` ```
+5 -3
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -100,7 +102,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
### 環境 ### 環境
1. Go 言語 1.21+ 1. Go 言語 1.23+
2. Flutter 3.16+ 2. Flutter 3.16+
### クローン ### クローン
@@ -160,7 +162,7 @@ gomobile init
- android - android
```bash ```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build apk flutter build apk
``` ```
+5 -3
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Trạng thái kiểm tra](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Trạng thái kiểm tra](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Phiên bản](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Phiên bản](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -108,7 +110,7 @@ Dự án này được chia thành hai phần, phần giao diện sử dụng `f
### Environment ### Environment
1. Golang 1.21+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.16+
### Clone ### Clone
@@ -168,7 +170,7 @@ command:
- android - android
```bash ```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build apk flutter build apk
``` ```
+5 -3
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -112,7 +114,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
### 环境要求 ### 环境要求
1. Golang 1.21+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.16+
### 克隆项目 ### 克隆项目
@@ -172,7 +174,7 @@ gomobile init
- android - android
```bash ```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build apk flutter build apk
``` ```
+5 -3
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,7 +7,7 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
@@ -108,7 +110,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
### 開發環境 ### 開發環境
1. Golang 1.21+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.16+
### 克隆項目 ### 克隆項目
@@ -168,7 +170,7 @@ gomobile init
- android - android
```bash ```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build apk flutter build apk
``` ```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 51 KiB

+26 -1
View File
@@ -8,7 +8,9 @@ import (
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
"net/http" "net/http"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"syscall"
) )
//go:embed banner.txt //go:embed banner.txt
@@ -25,8 +27,17 @@ func Start(cfg *model.StartConfig) {
panic(err) panic(err)
} }
if downloadCfg.FirstLoad { if downloadCfg.FirstLoad {
// Set default download config
if cfg.DownloadConfig != nil {
cfg.DownloadConfig.Merge(downloadCfg)
// TODO Use PatchConfig
rest.Downloader.PutConfig(cfg.DownloadConfig)
downloadCfg = cfg.DownloadConfig
}
downloadDir := downloadCfg.DownloadDir
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads // Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
var downloadDir string if downloadDir == "" {
if base.InDocker == "true" { if base.InDocker == "true" {
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads") downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
} else { } else {
@@ -40,8 +51,22 @@ func Start(cfg *model.StartConfig) {
rest.Downloader.PutConfig(downloadCfg) rest.Downloader.PutConfig(downloadCfg)
} }
} }
}
watchExit()
fmt.Printf("Server start success on http://%s\n", listener.Addr().String()) fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err) panic(err)
} }
} }
func watchExit() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
fmt.Printf("Server is shutting down due to signal: %s\n", sig)
rest.Downloader.Close()
os.Exit(0)
}()
}
+6 -2
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"os" "os"
"path/filepath" "path/filepath"
) )
@@ -15,6 +16,8 @@ type args struct {
Password *string `json:"password"` Password *string `json:"password"`
ApiToken *string `json:"apiToken"` ApiToken *string `json:"apiToken"`
StorageDir *string `json:"storageDir"` StorageDir *string `json:"storageDir"`
// DownloadConfig when the first time to start the server, it will be configured as initial value
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
configPath *string configPath *string
} }
@@ -25,7 +28,7 @@ func parse() *args {
cliArgs.Port = flag.Int("P", 9999, "Bind Port") cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username") cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd") cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.") cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
cliArgs.StorageDir = flag.String("d", "", "Storage directory") cliArgs.StorageDir = flag.String("d", "", "Storage directory")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path") cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse() flag.Parse()
@@ -57,8 +60,9 @@ func loadConfig(path string) *args {
var args args var args args
if !filepath.IsAbs(path) { if !filepath.IsAbs(path) {
dir, err := filepath.Abs(filepath.Dir(os.Args[0])) dir, err := os.Getwd()
if err != nil { if err != nil {
fmt.Println("config dir get failed, reason:" + err.Error())
return &args return &args
} }
path = filepath.Join(dir, path) path = filepath.Join(dir, path)
+1
View File
@@ -48,6 +48,7 @@ func main() {
Storage: model.StorageBolt, Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"), StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken, ApiToken: *args.ApiToken,
DownloadConfig: args.DownloadConfig,
ProductionMode: true, ProductionMode: true,
WebEnable: true, WebEnable: true,
WebFS: sub, WebFS: sub,
+8 -5
View File
@@ -1,12 +1,15 @@
version: '3'
services: services:
gopeed: gopeed:
container_name: gopeed container_name: gopeed
ports: ports:
- "9999:9999" # HTTP port (host:container) - 9999:9999 # HTTP port (host:container)
image: liwei2633/gopeed environment:
- PUID=0
- PGID=0
- UMASK=022
volumes: volumes:
- ~/gopeed/Downloads:/root/Downloads # mount download path - ~/gopeed/Downloads:/app/Downloads # mount download path
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line #- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
restart: unless-stopped restart: unless-stopped
image: liwei2633/gopeed
# command: -u Username -p Password # optional authentication
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/sh
chown -R ${PUID}:${PGID} /app chown -R ${PUID}:${PGID} /app
+71 -62
View File
@@ -1,118 +1,127 @@
module github.com/GopeedLab/gopeed module github.com/GopeedLab/gopeed
go 1.21 go 1.23.0
toolchain go1.23.1
require ( require (
github.com/anacrolix/missinggo/v2 v2.7.3 github.com/anacrolix/missinggo/v2 v2.8.0
github.com/anacrolix/torrent v1.53.3 github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
github.com/go-git/go-git/v5 v5.8.1 github.com/go-git/go-git/v5 v5.8.1
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0 github.com/matoous/go-nanoid/v2 v2.0.0
github.com/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
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
go.etcd.io/bbolt v1.3.8 go.etcd.io/bbolt v1.3.11
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
golang.org/x/sync v0.6.0 golang.org/x/sync v0.9.0
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
github.com/RoaringBitmap/roaring v1.7.0 // indirect github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
github.com/anacrolix/chansync v0.4.0 // indirect github.com/anacrolix/chansync v0.6.0 // indirect
github.com/anacrolix/dht/v2 v2.21.0 // indirect github.com/anacrolix/dht/v2 v2.22.0 // indirect
github.com/anacrolix/envpprof v1.3.0 // indirect github.com/anacrolix/envpprof v1.4.0 // indirect
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
github.com/anacrolix/go-libutp v1.3.1 // indirect github.com/anacrolix/go-libutp v1.3.1 // indirect
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 // indirect github.com/anacrolix/log v0.16.0 // indirect
github.com/anacrolix/missinggo v1.3.0 // indirect github.com/anacrolix/missinggo v1.3.0 // indirect
github.com/anacrolix/missinggo/perf v1.0.0 // indirect github.com/anacrolix/missinggo/perf v1.0.0 // indirect
github.com/anacrolix/mmsg v1.0.0 // indirect github.com/anacrolix/mmsg v1.1.1 // indirect
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect github.com/anacrolix/multiless v0.4.0 // indirect
github.com/anacrolix/stm v0.5.0 // indirect github.com/anacrolix/stm v0.5.0 // indirect
github.com/anacrolix/sync v0.5.1 // indirect github.com/anacrolix/sync v0.5.3 // indirect
github.com/anacrolix/upnp v0.1.3 // indirect github.com/anacrolix/upnp v0.1.4 // indirect
github.com/anacrolix/utp v0.2.0 // indirect github.com/anacrolix/utp v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/benbjohnson/immutable v0.4.3 // indirect github.com/benbjohnson/immutable v0.4.3 // indirect
github.com/bits-and-blooms/bitset v1.13.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/cloudflare/circl v1.3.3 // indirect github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-llsqlite/adapter v0.1.0 // indirect github.com/go-llsqlite/adapter v0.1.0 // indirect
github.com/go-llsqlite/crawshaw v0.5.0 // indirect github.com/go-llsqlite/crawshaw v0.5.5 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.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.2 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
github.com/google/uuid v1.5.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/huandu/xstrings v1.4.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/pion/datachannel v1.5.5 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/pion/dtls/v2 v2.2.9 // indirect github.com/multiformats/go-varint v0.0.7 // indirect
github.com/pion/ice/v2 v2.3.12 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pion/interceptor v0.1.25 // indirect github.com/pion/datachannel v1.5.9 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/ice/v4 v4.0.2 // indirect
github.com/pion/interceptor v0.1.37 // indirect
github.com/pion/logging v0.2.2 // indirect github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.9 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.13 // indirect github.com/pion/rtcp v1.2.14 // indirect
github.com/pion/rtp v1.8.3 // indirect github.com/pion/rtp v1.8.9 // indirect
github.com/pion/sctp v1.8.9 // indirect github.com/pion/sctp v1.8.33 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect github.com/pion/sdp/v3 v3.0.9 // indirect
github.com/pion/srtp/v2 v2.0.18 // indirect github.com/pion/srtp/v3 v3.0.4 // indirect
github.com/pion/stun v0.6.1 // indirect github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v2 v2.1.4 // indirect github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v3 v3.2.24 // indirect github.com/pion/webrtc/v4 v4.0.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
github.com/sergi/go-diff v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect github.com/skeema/knownhosts v1.2.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect github.com/tidwall/btree v1.7.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.17.0 // indirect golang.org/x/tools v0.27.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect
modernc.org/libc v1.40.6 // indirect modernc.org/libc v1.61.0 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.28.0 // indirect modernc.org/sqlite v1.33.1 // indirect
zombiezen.com/go/sqlite v1.1.0 // indirect zombiezen.com/go/sqlite v1.4.0 // indirect
) )
+224 -194
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -2,13 +2,15 @@ package controller
import ( import (
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
) )
type Controller struct { type Controller struct {
GetConfig func(v any) bool GetConfig func(v any)
ProxyConfig *base.DownloaderProxyConfig GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error)
FileController FileController
//ContextDialer() (proxy.Dialer, error) //ContextDialer() (proxy.Dialer, error)
} }
@@ -22,10 +24,9 @@ type DefaultFileController struct {
func NewController() *Controller { func NewController() *Controller {
return &Controller{ return &Controller{
GetConfig: func(v any) {},
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
FileController: &DefaultFileController{}, FileController: &DefaultFileController{},
GetConfig: func(v any) bool {
return false
},
} }
} }
+52 -8
View File
@@ -4,14 +4,12 @@ import (
"github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"path" "path"
"strings"
) )
// Fetcher defines the interface for a download protocol. // Fetcher defines the interface for a download protocol.
// One fetcher for each download task // Each download task will have a corresponding Fetcher instance for the management of the download task
type Fetcher interface { type Fetcher interface {
// Name return the name of the protocol.
Name() string
Setup(ctl *controller.Controller) Setup(ctl *controller.Controller)
// Resolve resource info from request // Resolve resource info from request
Resolve(req *base.Request) error Resolve(req *base.Request) error
@@ -31,6 +29,12 @@ type Fetcher interface {
Wait() error Wait() error
} }
type Uploader interface {
Upload() error
UploadedBytes() int64
WaitUpload() error
}
// FetcherMeta defines the meta information of a fetcher. // FetcherMeta defines the meta information of a fetcher.
type FetcherMeta struct { type FetcherMeta struct {
Req *base.Request `json:"req"` Req *base.Request `json:"req"`
@@ -68,17 +72,57 @@ func (m *FetcherMeta) RootDirPath() string {
} }
} }
// FetcherBuilder defines the interface for a fetcher builder. type FilterType int
type FetcherBuilder interface {
// Schemes returns the schemes supported by the fetcher. const (
Schemes() []string // FilterTypeUrl url type, pattern is the scheme, e.g. http://github.com -> http
FilterTypeUrl FilterType = iota
// FilterTypeFile file type, pattern is the file extension name, e.g. test.torrent -> torrent
FilterTypeFile
// FilterTypeBase64 base64 data type, pattern is the data mime type, e.g. data:application/x-bittorrent;base64 -> application/x-bittorrent
FilterTypeBase64
)
type SchemeFilter struct {
Type FilterType
Pattern string
}
func (s *SchemeFilter) Match(uri string) bool {
uriUpper := strings.ToUpper(uri)
patternUpper := strings.ToUpper(s.Pattern)
switch s.Type {
case FilterTypeUrl:
return strings.HasPrefix(uriUpper, patternUpper+":")
case FilterTypeFile:
return strings.HasSuffix(uriUpper, "."+patternUpper)
case FilterTypeBase64:
return strings.HasPrefix(uriUpper, "DATA:"+patternUpper+";BASE64,")
}
return false
}
// FetcherManager manage and control the fetcher
type FetcherManager interface {
// Name return the name of the protocol.
Name() string
// Filters registers the supported schemes.
Filters() []*SchemeFilter
// Build returns a new fetcher. // Build returns a new fetcher.
Build() Fetcher Build() Fetcher
// ParseName name displayed when the task is not yet resolved, parsed from the request URL
ParseName(u string) string
// AutoRename returns whether the fetcher need renaming the download file when has the same name file.
AutoRename() bool
// DefaultConfig returns the default configuration of the protocol.
DefaultConfig() any
// Store fetcher // Store fetcher
Store(fetcher Fetcher) (any, error) Store(fetcher Fetcher) (any, error)
// Restore fetcher // Restore fetcher
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher) Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
// Close the fetcher manager, release resources.
Close() error
} }
type DefaultFetcher struct { type DefaultFetcher struct {
+97
View File
@@ -0,0 +1,97 @@
package fetcher
import "testing"
func TestSchemeFilter_Match(t *testing.T) {
type fields struct {
Type FilterType
Pattern string
}
type args struct {
uri string
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "url match",
fields: fields{
Type: FilterTypeUrl,
Pattern: "https",
},
args: args{
uri: "https://github.com",
},
want: true,
},
{
name: "url not match",
fields: fields{
Type: FilterTypeUrl,
Pattern: "https",
},
args: args{
uri: "ftp://github.com",
},
want: false,
},
{
name: "file match",
fields: fields{
Type: FilterTypeFile,
Pattern: "torrent",
},
args: args{
uri: "d:/temp/test.torrent",
},
want: true,
},
{
name: "file not match",
fields: fields{
Type: FilterTypeFile,
Pattern: "torrent",
},
args: args{
uri: "d:/temp/test.txt",
},
want: false,
},
{
name: "base64 match",
fields: fields{
Type: FilterTypeBase64,
Pattern: "application/x-bittorrent",
},
args: args{
uri: "data:application/x-bittorrent;base64,xxx",
},
want: true,
},
{
name: "base64 not match",
fields: fields{
Type: FilterTypeBase64,
Pattern: "application/x-bittorrent",
},
args: args{
uri: "data:application/javascript;base64,xxx",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SchemeFilter{
Type: tt.fields.Type,
Pattern: tt.fields.Pattern,
}
if got := s.Match(tt.args.uri); got != tt.want {
t.Errorf("Match() = %v, want %v", got, tt.want)
}
})
}
}
+6
View File
@@ -3,4 +3,10 @@ package bt
type config struct { 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 bool `json:"seedKeep`
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
SeedRatio float64 `json:"seedRatio"`
// SeedTime is the time in seconds to seed after downloading is complete.
SeedTime int64 `json:"seedTime"`
} }
@@ -0,0 +1,49 @@
package bt
import (
"context"
"net"
"time"
"github.com/rs/dnscache"
)
// DnsCacheResolver resolves DNS requests for an HTTP client using an in-memory cache.
type DnsCacheResolver struct {
RefreshTimeout time.Duration
resolver dnscache.Resolver
}
func (r *DnsCacheResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ips, err := r.resolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
var conn net.Conn
for _, ip := range ips {
var dialer net.Dialer
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip, port))
if err == nil {
break
}
}
return conn, err
}
func (r *DnsCacheResolver) Run(ctx context.Context) {
ticker := time.NewTicker(r.RefreshTimeout)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.resolver.Refresh(true)
}
}
}
File diff suppressed because it is too large. Load diff
+115 -11
View File
@@ -8,6 +8,8 @@ import (
"github.com/GopeedLab/gopeed/internal/test" "github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt" "github.com/GopeedLab/gopeed/pkg/protocol/bt"
gohttp "net/http"
"net/url"
"os" "os"
"reflect" "reflect"
"testing" "testing"
@@ -33,7 +35,6 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
} }
want := &base.Resource{ want := &base.Resource{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112, Size: 1466714112,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
@@ -69,6 +70,7 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
} }
func doResolve(t *testing.T, fetcher fetcher.Fetcher) { func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
t.Run("Resolve Single File", func(t *testing.T) {
err := fetcher.Resolve(&base.Request{ err := fetcher.Resolve(&base.Request{
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent", URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
Extra: bt.ReqExtra{ Extra: bt.ReqExtra{
@@ -83,7 +85,6 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
} }
want := &base.Resource{ want := &base.Resource{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112, Size: 1466714112,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
@@ -95,31 +96,134 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed", Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
} }
if !reflect.DeepEqual(want, fetcher.Meta().Res) { if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want) t.Errorf("Resolve Single File Resolve() got = %v, want %v", fetcher.Meta().Res, want)
}
})
t.Run("Resolve Multi Files", func(t *testing.T) {
err := fetcher.Resolve(&base.Request{
URL: "./testdata/test.torrent",
Extra: bt.ReqExtra{
Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
},
},
})
if err != nil {
panic(err)
}
want := &base.Resource{
Name: "test",
Size: 107484864,
Range: true,
Files: []*base.FileInfo{
{
Name: "c.txt",
Path: "path",
Size: 98501754,
},
{
Name: "b.txt",
Size: 8904996,
},
{
Name: "a.txt",
Size: 78114,
},
},
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
}
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve Multi Files Resolve() got = %v, want %v", fetcher.Meta().Res, want)
}
})
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
err := fetcher.Resolve(&base.Request{
URL: "./testdata/test.unclean.torrent",
})
if err != nil {
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
}
})
}
func TestFetcherManager_ParseName(t *testing.T) {
type args struct {
u string
}
tests := []struct {
name string
args args
want string
}{
{
name: "broken url",
args: args{
u: "magnet://!@#%github.com",
},
want: "",
},
{
name: "dn",
args: args{
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed&dn=ubuntu-22.04-live-server-amd64.iso",
},
want: "ubuntu-22.04-live-server-amd64.iso",
},
{
name: "no dn",
args: args{
u: "magnet:?xt=urn:btih:8a55cfbd5ca5d11507364765936c4f9e55b253ed",
},
want: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
},
{
name: "non standard magnet",
args: args{
u: "magnet:?xxt=abcd",
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fm := &FetcherManager{}
if got := fm.ParseName(tt.args.u); got != tt.want {
t.Errorf("ParseName() = %v, want %v", got, tt.want)
}
})
} }
} }
func buildFetcher() fetcher.Fetcher { func buildFetcher() fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build() fb := new(FetcherManager)
fetcher.Setup(controller.NewController()) fetcher := fb.Build()
newController := controller.NewController()
newController.GetConfig = func(v any) {
json.Unmarshal([]byte(test.ToJson(fb.DefaultConfig())), v)
}
fetcher.Setup(newController)
return fetcher return fetcher
} }
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher { func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build() fetcher := new(FetcherManager).Build()
newController := controller.NewController() newController := controller.NewController()
mockCfg := config{ mockCfg := config{
Trackers: []string{ Trackers: []string{
"udp://tracker.birkenwald.de:6969/announce", "udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce", "udp://tracker.bitsearch.to:1337/announce",
}} }}
newController.GetConfig = func(v any) bool { newController.GetConfig = func(v any) {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil { json.Unmarshal([]byte(test.ToJson(mockCfg)), v)
return false
} }
return true newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
return proxyConfig.ToHandler()
} }
newController.ProxyConfig = proxyConfig
fetcher.Setup(newController) fetcher.Setup(newController)
return fetcher return fetcher
} }
+2 -1
View File
@@ -3,6 +3,7 @@
package bt package bt
import ( import (
"context"
"fmt" "fmt"
"github.com/anacrolix/torrent/storage" "github.com/anacrolix/torrent/storage"
"io" "io"
@@ -53,7 +54,7 @@ func (fs fileClientImpl) Close() error {
return fs.opts.PieceCompletion.Close() return fs.opts.PieceCompletion.Close()
} }
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) { func (fs fileClientImpl) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
upvertedFiles := info.UpvertedFiles() upvertedFiles := info.UpvertedFiles()
files := make([]file, 0, len(upvertedFiles)) files := make([]file, 0, len(upvertedFiles))
for _, fileInfo := range upvertedFiles { for _, fileInfo := range upvertedFiles {
Binary file not shown.
Binary file not shown.
+116 -61
View File
@@ -3,16 +3,12 @@ package http
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/xiaoqidun/setft"
"golang.org/x/sync/errgroup"
"io" "io"
"mime" "mime"
"net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
@@ -21,8 +17,18 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/xiaoqidun/setft"
"golang.org/x/sync/errgroup"
) )
const connectTimeout = 15 * time.Second
const readTimeout = 15 * time.Second
type RequestError struct { type RequestError struct {
Code int Code int
Msg string Msg string
@@ -64,23 +70,13 @@ type Fetcher struct {
eg *errgroup.Group eg *errgroup.Group
} }
func (f *Fetcher) Name() string {
return "http"
}
func (f *Fetcher) Setup(ctl *controller.Controller) { func (f *Fetcher) Setup(ctl *controller.Controller) {
f.ctl = ctl f.ctl = ctl
f.doneCh = make(chan error, 1) f.doneCh = make(chan error, 1)
if f.meta == nil { if f.meta == nil {
f.meta = &fetcher.FetcherMeta{} f.meta = &fetcher.FetcherMeta{}
} }
exist := f.ctl.GetConfig(&f.config) f.ctl.GetConfig(&f.config)
if !exist {
f.config = &config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
Connections: 1,
}
}
return return
} }
@@ -88,6 +84,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil { if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
return err return err
} }
f.meta.Req = req
httpReq, err := f.buildRequest(nil, req) httpReq, err := f.buildRequest(nil, req)
if err != nil { if err != nil {
return err return err
@@ -108,9 +105,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
} }
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) { if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载 // response 206 status code, support breakpoint continuation
res.Range = true res.Range = true
// 解析资源大小: bytes 0-1000/1001 => 1001 // parse content length from Content-Range header, eg: bytes 0-1000/1001
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange)) contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
if contentTotal != "" { if contentTotal != "" {
parse, err := strconv.ParseInt(contentTotal, 10, 64) parse, err := strconv.ParseInt(contentTotal, 10, 64)
@@ -120,7 +117,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
res.Size = parse res.Size = parse
} }
} else if base.HttpCodeOK == httpResp.StatusCode { } else if base.HttpCodeOK == httpResp.StatusCode {
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码 // response 200 status code, not support breakpoint continuation, get file size by Content-Length header
// if not found, maybe chunked encoding
contentLength := httpResp.Header.Get(base.HttpHeaderContentLength) contentLength := httpResp.Header.Get(base.HttpHeaderContentLength)
if contentLength != "" { if contentLength != "" {
parse, err := strconv.ParseInt(contentLength, 10, 64) parse, err := strconv.ParseInt(contentLength, 10, 64)
@@ -152,7 +150,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
file.Name = filename file.Name = filename
} }
} }
// Get file filePath by URL // get file filePath by URL
if file.Name == "" { if file.Name == "" {
file.Name = path.Base(httpReq.URL.Path) file.Name = path.Base(httpReq.URL.Path)
} }
@@ -161,7 +159,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
file.Name = httpReq.URL.Hostname() file.Name = httpReq.URL.Hostname()
} }
res.Files = append(res.Files, file) res.Files = append(res.Files, file)
f.meta.Req = req
f.meta.Res = res f.meta.Res = res
return nil return nil
} }
@@ -169,9 +166,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
func (f *Fetcher) Create(opts *base.Options) error { func (f *Fetcher) Create(opts *base.Options) error {
f.meta.Opts = opts f.meta.Opts = opts
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
return err
}
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil { if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
return err return err
} }
@@ -179,8 +173,12 @@ func (f *Fetcher) Create(opts *base.Options) error {
opts.Extra = &fhttp.OptsExtra{} opts.Extra = &fhttp.OptsExtra{}
} }
extra := opts.Extra.(*fhttp.OptsExtra) extra := opts.Extra.(*fhttp.OptsExtra)
if extra.Connections == 0 { if extra.Connections <= 0 {
extra.Connections = f.config.Connections extra.Connections = f.config.Connections
// Avoid zero connections configuration
if extra.Connections <= 0 {
extra.Connections = 1
}
} }
return nil return nil
} }
@@ -201,6 +199,12 @@ func (f *Fetcher) Start() (err error) {
if err != nil { if err != nil {
return err return err
} }
// Avoid request extra modified by extension
if err = base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
return err
}
if f.chunks == nil { if f.chunks == nil {
f.chunks = f.splitChunk() f.chunks = f.splitChunk()
} }
@@ -250,23 +254,42 @@ func (f *Fetcher) Wait() (err error) {
return <-f.doneCh return <-f.doneCh
} }
type fetchResult struct {
err error
}
func (f *Fetcher) fetch() { 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))
for i := 0; i < len(f.chunks); i++ { for i := 0; i < len(f.chunks); i++ {
i := i i := i
f.eg.Go(func() error { f.eg.Go(func() error {
return f.fetchChunk(i, ctx) err := f.fetchChunk(i, ctx)
// if canceled, fail fast
if errors.Is(err, context.Canceled) {
return err
}
chunkErrs[i] = err
return nil
}) })
} }
go func() { go func() {
err := f.eg.Wait() err := f.eg.Wait()
// check if canceled // error returned only if canceled, just return
if errors.Is(err, context.Canceled) { if err != nil {
return return
} }
// check all fetch results, if any error, return
for _, chunkErr := range chunkErrs {
if chunkErr != nil {
err = chunkErr
break
}
}
f.file.Close() f.file.Close()
// Update file last modified time // Update file last modified time
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil { if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
@@ -289,25 +312,12 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
for { for {
// if chunk is completed, return // if chunk is completed, return
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 { if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
return return nil
} }
if chunk.retryTimes >= maxRetries { if chunk.retryTimes >= maxRetries {
if !f.meta.Res.Range {
return return
} }
// check if all failed
allFailed := true
for _, c := range f.chunks {
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
allFailed = false
break
}
}
if allFailed {
return
}
}
var ( var (
httpReq *http.Request httpReq *http.Request
@@ -333,10 +343,9 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
err = NewRequestError(resp.StatusCode, resp.Status) err = NewRequestError(resp.StatusCode, resp.Status)
return err return err
} }
// Http request success, reset retry times reader := NewTimeoutReader(resp.Body, readTimeout)
chunk.retryTimes = 0
for { for {
n, err := resp.Body.Read(buf) n, err := reader.Read(buf)
if n > 0 { if n > 0 {
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded) _, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
if err != nil { if err != nil {
@@ -351,7 +360,6 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return err return err
} }
} }
return nil
}() }()
if err != nil { if err != nil {
// If canceled, do not retry // If canceled, do not retry
@@ -386,7 +394,8 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
method string method string
body io.Reader body io.Reader
) )
headers := make(map[string][]string)
headers := http.Header{}
if req.Extra == nil { if req.Extra == nil {
method = http.MethodGet method = http.MethodGet
} else { } else {
@@ -398,7 +407,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[k] = []string{v} headers.Set(k, v)
} }
} }
if extra.Body != "" { if extra.Body != "" {
@@ -406,8 +415,7 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
} }
} }
if _, ok := headers[base.HttpHeaderUserAgent]; !ok { if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
// load user agent from config headers.Set(base.HttpHeaderUserAgent, f.config.UserAgent)
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
} }
if ctx != nil { if ctx != nil {
@@ -452,7 +460,13 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
func (f *Fetcher) buildClient() *http.Client { func (f *Fetcher) buildClient() *http.Client {
transport := &http.Transport{ transport := &http.Transport{
Proxy: f.ctl.ProxyConfig.ToHandler(), DialContext: (&net.Dialer{
Timeout: connectTimeout,
}).DialContext,
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
},
} }
// Cookie handle // Cookie handle
jar, _ := cookiejar.New(nil) jar, _ := cookiejar.New(nil)
@@ -466,30 +480,67 @@ type fetcherData struct {
Chunks []*chunk Chunks []*chunk
} }
type FetcherBuilder struct { type FetcherManager struct {
} }
var schemes = []string{"HTTP", "HTTPS"} func (fm *FetcherManager) Name() string {
return "http"
func (fb *FetcherBuilder) Schemes() []string {
return schemes
} }
func (fb *FetcherBuilder) Build() fetcher.Fetcher { func (fm *FetcherManager) Filters() []*fetcher.SchemeFilter {
return []*fetcher.SchemeFilter{
{
Type: fetcher.FilterTypeUrl,
Pattern: "HTTP",
},
{
Type: fetcher.FilterTypeUrl,
Pattern: "HTTPS",
},
}
}
func (fm *FetcherManager) Build() fetcher.Fetcher {
return &Fetcher{} return &Fetcher{}
} }
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) { func (fm *FetcherManager) ParseName(u string) string {
var name string
url, err := url.Parse(u)
if err != nil {
return ""
}
// Get filePath by URL
name = path.Base(url.Path)
// If file name is empty, use host name
if name == "" || name == "/" || name == "." {
name = url.Hostname()
}
return name
}
func (fm *FetcherManager) AutoRename() bool {
return true
}
func (fm *FetcherManager) DefaultConfig() any {
return &config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
Connections: 16,
}
}
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
_f := f.(*Fetcher) _f := f.(*Fetcher)
return &fetcherData{ return &fetcherData{
Chunks: _f.chunks, Chunks: _f.chunks,
}, nil }, nil
} }
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) { func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher { return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
fd := v.(*fetcherData) fd := v.(*fetcherData)
fb := &FetcherBuilder{} fb := &FetcherManager{}
fetcher := fb.Build().(*Fetcher) fetcher := fb.Build().(*Fetcher)
fetcher.meta = meta fetcher.meta = meta
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req) base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
@@ -500,3 +551,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
return fetcher return fetcher
} }
} }
func (fm *FetcherManager) Close() error {
return nil
}
+72 -10
View File
@@ -9,6 +9,8 @@ import (
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"net" "net"
gohttp "net/http"
"net/url"
"testing" "testing"
"time" "time"
) )
@@ -130,6 +132,14 @@ func TestFetcher_DownloadLimit(t *testing.T) {
downloadNormal(listener, 8, t) downloadNormal(listener, 8, t)
} }
func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
listener := test.StartTestLimitServer(16, readTimeout.Milliseconds()+5000)
defer listener.Close()
downloadError(listener, 1, t)
downloadError(listener, 4, t)
}
func TestFetcher_DownloadResume(t *testing.T) { func TestFetcher_DownloadResume(t *testing.T) {
listener := test.StartTestFileServer() listener := test.StartTestFileServer()
defer listener.Close() defer listener.Close()
@@ -192,6 +202,54 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
} }
} }
func TestFetcherManager_ParseName(t *testing.T) {
type args struct {
u string
}
tests := []struct {
name string
args args
want string
}{
{
name: "broken url",
args: args{
u: "https://!@#%github.com",
},
want: "",
},
{
name: "file path",
args: args{
u: "https://github.com/index.html",
},
want: "index.html",
},
{
name: "file path with query and hash",
args: args{
u: "https://github.com/a/b/index.html/#list?name=1",
},
want: "index.html",
},
{
name: "no file path",
args: args{
u: "https://github.com",
},
want: "github.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fm := &FetcherManager{}
if got := fm.ParseName(tt.args.u); got != tt.want {
t.Errorf("ParseName() = %v, want %v", got, tt.want)
}
})
}
}
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher { func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
return doDownloadReady(buildFetcher(), listener, connections, t) return doDownloadReady(buildFetcher(), listener, connections, t)
} }
@@ -306,7 +364,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fb := new(FetcherBuilder) fb := new(FetcherManager)
time.Sleep(time.Millisecond * 50) time.Sleep(time.Millisecond * 50)
data, err := fb.Store(fetcher) data, err := fb.Store(fetcher)
if err != nil { if err != nil {
@@ -337,10 +395,12 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) { func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
fetcher := downloadReady(httpListener, 4, t) fetcher := downloadReady(httpListener, 4, t)
ctl := controller.NewController() ctl := controller.NewController()
ctl.ProxyConfig = &base.DownloaderProxyConfig{ ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
return (&base.DownloaderProxyConfig{
Enable: true, Enable: true,
Scheme: "socks5", Scheme: "socks5",
Host: proxyListener.Addr().String(), Host: proxyListener.Addr().String(),
}).ToHandler()
} }
fetcher.Setup(ctl) fetcher.Setup(ctl)
err := fetcher.Start() err := fetcher.Start()
@@ -359,19 +419,21 @@ func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t
} }
func buildFetcher() *Fetcher { func buildFetcher() *Fetcher {
fetcher := new(FetcherBuilder).Build() fm := new(FetcherManager)
fetcher.Setup(controller.NewController()) fetcher := fm.Build()
newController := controller.NewController()
newController.GetConfig = func(v any) {
json.Unmarshal([]byte(test.ToJson(fm.DefaultConfig())), v)
}
fetcher.Setup(newController)
return fetcher.(*Fetcher) return fetcher.(*Fetcher)
} }
func buildConfigFetcher(cfg config) fetcher.Fetcher { func buildConfigFetcher(cfg config) fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build() fetcher := new(FetcherManager).Build()
newController := controller.NewController() newController := controller.NewController()
newController.GetConfig = func(v any) bool { newController.GetConfig = func(v any) {
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil { json.Unmarshal([]byte(test.ToJson(cfg)), v)
return false
}
return true
} }
fetcher.Setup(newController) fetcher.Setup(newController)
return fetcher return fetcher
+40
View File
@@ -0,0 +1,40 @@
package http
import (
"context"
"io"
"time"
)
type TimeoutReader struct {
reader io.Reader
timeout time.Duration
}
func NewTimeoutReader(r io.Reader, timeout time.Duration) *TimeoutReader {
return &TimeoutReader{
reader: r,
timeout: timeout,
}
}
func (tr *TimeoutReader) Read(p []byte) (n int, err error) {
ctx, cancel := context.WithTimeout(context.Background(), tr.timeout)
defer cancel()
done := make(chan struct{})
var readErr error
var bytesRead int
go func() {
bytesRead, readErr = tr.reader.Read(p)
close(done)
}()
select {
case <-done:
return bytesRead, readErr
case <-ctx.Done():
return 0, ctx.Err()
}
}
@@ -0,0 +1,51 @@
package http
import (
"bytes"
"context"
"errors"
"io"
"testing"
"time"
)
func TestTimeoutReader_Read(t *testing.T) {
data := []byte("Hello, World!")
reader := bytes.NewReader(data)
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
buf := make([]byte, len(data))
n, err := timeoutReader.Read(buf)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if n != len(data) {
t.Fatalf("expected to read %d bytes, read %d", len(data), n)
}
if !bytes.Equal(buf, data) {
t.Fatalf("expected %s, got %s", data, buf)
}
}
func TestTimeoutReader_ReadTimeout(t *testing.T) {
reader := &slowReader{delay: 2 * time.Second}
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
buf := make([]byte, 8192)
_, err := timeoutReader.Read(buf)
if err == nil {
t.Fatal("expected timeout error, got nil")
}
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("expected %v, got %v", context.DeadlineExceeded, err)
}
}
type slowReader struct {
delay time.Duration
}
func (sr *slowReader) Read(p []byte) (n int, err error) {
time.Sleep(sr.delay)
return 0, io.EOF
}
+30 -19
View File
@@ -2,12 +2,13 @@ package test
import ( import (
"context" "context"
"crypto/rand"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/armon/go-socks5" "github.com/armon/go-socks5"
"io" "io"
"math/rand"
"net" "net"
"net/http" "net/http"
"os" "os"
@@ -36,7 +37,7 @@ const (
) )
func StartTestFileServer() net.Listener { func StartTestFileServer() net.Listener {
return startTestServer(func() http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
return http.FileServer(http.Dir(Dir)) return http.FileServer(http.Dir(Dir))
}) })
} }
@@ -52,7 +53,7 @@ func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func StartTestSlowFileServer(delay time.Duration) net.Listener { func StartTestSlowFileServer(delay time.Duration) net.Listener {
return startTestServer(func() http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
return &SlowFileServer{ return &SlowFileServer{
delay: delay, delay: delay,
handler: http.FileServer(http.Dir(Dir)), handler: http.FileServer(http.Dir(Dir)),
@@ -61,7 +62,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
} }
func StartTestCustomServer() net.Listener { func StartTestCustomServer() net.Listener {
return startTestServer(func() http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) { mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
file, err := os.Open(BuildFile) file, err := os.Open(BuildFile)
@@ -88,7 +89,7 @@ func StartTestCustomServer() net.Listener {
func StartTestRetryServer() net.Listener { func StartTestRetryServer() net.Listener {
counter := 0 counter := 0
return startTestServer(func() http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) { mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
counter++ counter++
@@ -108,7 +109,7 @@ func StartTestRetryServer() net.Listener {
} }
func StartTestPostServer() net.Listener { func StartTestPostServer() net.Listener {
return startTestServer(func() http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) { mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
if request.Method == "POST" && request.Header.Get("Authorization") != "" { if request.Method == "POST" && request.Header.Get("Authorization") != "" {
@@ -132,7 +133,7 @@ func StartTestPostServer() net.Listener {
func StartTestErrorServer() net.Listener { func StartTestErrorServer() net.Listener {
counter := 0 counter := 0
return startTestServer(func() http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) { mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
counter++ counter++
@@ -149,7 +150,7 @@ func StartTestErrorServer() net.Listener {
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener { func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
var connections atomic.Int32 var connections atomic.Int32
return startTestServer(func() http.Handler { return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) { mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
defer func() { defer func() {
@@ -165,12 +166,14 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
if r == "" { if r == "" {
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize)) writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
writer.WriteHeader(200) writer.WriteHeader(200)
(writer.(http.Flusher)).Flush()
file, err := os.Open(BuildFile) file, err := os.Open(BuildFile)
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer file.Close() defer file.Close()
slowCopy(writer, file, delay) slowCopy(sl, writer, file, delay)
} else { } else {
// split range // split range
s := strings.Split(r, "=") s := strings.Split(r, "=")
@@ -204,6 +207,8 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize)) writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
writer.Header().Set("Accept-Ranges", "bytes") writer.Header().Set("Accept-Ranges", "bytes")
writer.WriteHeader(206) writer.WriteHeader(206)
(writer.(http.Flusher)).Flush()
file, err := os.Open(BuildFile) file, err := os.Open(BuildFile)
if err != nil { if err != nil {
writer.WriteHeader(500) writer.WriteHeader(500)
@@ -211,7 +216,7 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
} }
defer file.Close() defer file.Close()
file.Seek(start, 0) file.Seek(start, 0)
slowCopyN(writer, file, end-start+1, delay) slowCopyN(sl, writer, file, end-start+1, delay)
} }
}) })
return mux return mux
@@ -219,9 +224,12 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
} }
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second // slowCopyN copies n bytes from src to dst, speed limit is bytes per second
func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err error) { func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
buf := make([]byte, 32*1024) buf := make([]byte, 32*1024)
for { for {
if sl.isShutdown {
return 0, errors.New("server shutdown")
}
nr, er := src.Read(buf) nr, er := src.Read(buf)
if nr > 0 { if nr > 0 {
nw, ew := dst.Write(buf[0:nr]) nw, ew := dst.Write(buf[0:nr])
@@ -250,8 +258,8 @@ func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err err
return written, err return written, err
} }
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) { func slowCopyN(sl *shutdownListener, dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
written, err = slowCopy(dst, io.LimitReader(src, n), delay) written, err = slowCopy(sl, dst, io.LimitReader(src, n), delay)
if written == n { if written == n {
return n, nil return n, nil
} }
@@ -262,7 +270,7 @@ func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int6
return return
} }
func startTestServer(serverHandle func() http.Handler) net.Listener { func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.Listener {
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
panic(err) panic(err)
@@ -272,7 +280,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
panic(err) panic(err)
} }
defer file.Close() defer file.Close()
// 随机生成一个文件 // Write random data
l := int64(8192) l := int64(8192)
buf := make([]byte, l) buf := make([]byte, l)
size := int64(0) size := int64(0)
@@ -289,21 +297,24 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
size += l size += l
} }
server := &http.Server{} server := &http.Server{}
server.Handler = serverHandle() sl := &shutdownListener{
go server.Serve(listener)
return &shutdownListener{
server: server, server: server,
Listener: listener, Listener: listener,
} }
server.Handler = serverHandle(sl)
go server.Serve(listener)
return sl
} }
type shutdownListener struct { type shutdownListener struct {
server *http.Server server *http.Server
isShutdown bool
net.Listener net.Listener
} }
func (c *shutdownListener) Close() error { func (c *shutdownListener) Close() error {
c.isShutdown = true
closeErr := c.server.Shutdown(context.Background()) closeErr := c.server.Shutdown(context.Background())
if err := ifExistAndRemove(BuildFile); err != nil { if err := ifExistAndRemove(BuildFile); err != nil {
fmt.Println(err) fmt.Println(err)
-8
View File
@@ -1,8 +0,0 @@
package base
import "errors"
var (
NotFound = errors.New("not found")
BadParams = errors.New("bad params")
)
+73 -8
View File
@@ -7,6 +7,7 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"net/http" "net/http"
"net/url" "net/url"
"sync"
"time" "time"
) )
@@ -16,6 +17,10 @@ type Request struct {
Extra any `json:"extra"` Extra any `json:"extra"`
// Labels is used to mark the download task // Labels is used to mark the download task
Labels map[string]string `json:"labels"` Labels map[string]string `json:"labels"`
// Proxy is special proxy config for request
Proxy *RequestProxy `json:"proxy"`
// SkipVerifyCert is the flag that skip verify cert
SkipVerifyCert bool `json:"skipVerifyCert"`
} }
func (r *Request) Validate() error { func (r *Request) Validate() error {
@@ -25,6 +30,37 @@ func (r *Request) Validate() error {
return nil return nil
} }
type RequestProxyMode string
const (
// RequestProxyModeFollow follow setting proxy
RequestProxyModeFollow RequestProxyMode = "follow"
// RequestProxyModeNone not use proxy
RequestProxyModeNone RequestProxyMode = "none"
// RequestProxyModeCustom custom proxy
RequestProxyModeCustom RequestProxyMode = "custom"
)
type RequestProxy struct {
Mode RequestProxyMode `json:"mode"`
Scheme string `json:"scheme"`
Host string `json:"host"`
Usr string `json:"usr"`
Pwd string `json:"pwd"`
}
func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) {
if p == nil || p.Mode != RequestProxyModeCustom {
return nil
}
if p.Scheme == "" || p.Host == "" {
return nil
}
return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd))
}
// Resource download resource // Resource download resource
type Resource struct { type Resource struct {
// if name is not empty, the resource is a folder and the name is the folder name // if name is not empty, the resource is a folder and the name is the folder name
@@ -94,12 +130,7 @@ func (o *Options) InitSelectFiles(fileSize int) {
} }
func (o *Options) Clone() *Options { func (o *Options) Clone() *Options {
return &Options{ return util.DeepClone(o)
Name: o.Name,
Path: o.Path,
SelectFiles: o.SelectFiles,
Extra: o.Extra,
}
} }
func ParseReqExtra[E any](req *Request) error { func ParseReqExtra[E any](req *Request) error {
@@ -147,12 +178,37 @@ func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.MaxRunning == 0 { if cfg.MaxRunning == 0 {
cfg.MaxRunning = 5 cfg.MaxRunning = 5
} }
if cfg.ProtocolConfig == nil {
cfg.ProtocolConfig = make(map[string]any)
}
if cfg.Proxy == nil { if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{} cfg.Proxy = &DownloaderProxyConfig{}
} }
return cfg return cfg
} }
func (cfg *DownloaderStoreConfig) Merge(beforeCfg *DownloaderStoreConfig) *DownloaderStoreConfig {
if beforeCfg == nil {
return cfg
}
if cfg.DownloadDir == "" {
cfg.DownloadDir = beforeCfg.DownloadDir
}
if cfg.MaxRunning == 0 {
cfg.MaxRunning = beforeCfg.MaxRunning
}
if cfg.ProtocolConfig == nil {
cfg.ProtocolConfig = beforeCfg.ProtocolConfig
}
if cfg.Extra == nil {
cfg.Extra = beforeCfg.Extra
}
if cfg.Proxy == nil {
cfg.Proxy = beforeCfg.Proxy
}
return cfg
}
type DownloaderProxyConfig struct { type DownloaderProxyConfig struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
// System is the flag that use system proxy // System is the flag that use system proxy
@@ -168,7 +224,7 @@ func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, e
return nil return nil
} }
if cfg.System { if cfg.System {
ieproxy.ReloadConf() safeProxyReloadConf()
return ieproxy.GetProxyFunc() return ieproxy.GetProxyFunc()
} }
if cfg.Scheme == "" || cfg.Host == "" { if cfg.Scheme == "" || cfg.Host == "" {
@@ -183,7 +239,7 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
return nil return nil
} }
if cfg.System { if cfg.System {
ieproxy.ReloadConf() safeProxyReloadConf()
static := ieproxy.GetConf().Static static := ieproxy.GetConf().Static
if static.Active && len(static.Protocols) > 0 { if static.Active && len(static.Protocols) > 0 {
// If only one protocol, use it // If only one protocol, use it
@@ -209,6 +265,15 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd) return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
} }
var prcLock sync.Mutex
func safeProxyReloadConf() {
prcLock.Lock()
defer prcLock.Unlock()
ieproxy.ReloadConf()
}
func parseUrlSafe(rawUrl string) *url.URL { func parseUrlSafe(rawUrl string) *url.URL {
u, err := url.Parse(rawUrl) u, err := url.Parse(rawUrl)
if err != nil { if err != nil {
+264
View File
@@ -0,0 +1,264 @@
package base
import (
"reflect"
"testing"
)
func TestDownloaderStoreConfig_Init(t *testing.T) {
tests := []struct {
name string
fields *DownloaderStoreConfig
want *DownloaderStoreConfig
}{
{
"Init",
&DownloaderStoreConfig{},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
},
},
{
"Init MaxRunning",
&DownloaderStoreConfig{
MaxRunning: 10,
},
&DownloaderStoreConfig{
MaxRunning: 10,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
},
},
{
"Init ProtocolConfig",
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "value",
},
},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{
"key": "value",
},
Proxy: &DownloaderProxyConfig{},
},
},
{
"Init Proxy",
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Enable: true,
},
},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{
Enable: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &DownloaderStoreConfig{
FirstLoad: tt.fields.FirstLoad,
DownloadDir: tt.fields.DownloadDir,
MaxRunning: tt.fields.MaxRunning,
ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
}
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Init() = %v, want %v", got, tt.want)
}
})
}
}
func TestDownloaderStoreConfig_Merge(t *testing.T) {
type args struct {
beforeCfg *DownloaderStoreConfig
}
tests := []struct {
name string
fields *DownloaderStoreConfig
args args
want *DownloaderStoreConfig
}{
{
"Merge Nil",
&DownloaderStoreConfig{},
args{
beforeCfg: nil,
},
&DownloaderStoreConfig{},
},
{
"Merge DownloadDir No Override",
&DownloaderStoreConfig{
DownloadDir: "before",
},
args{
beforeCfg: &DownloaderStoreConfig{
DownloadDir: "after",
},
},
&DownloaderStoreConfig{
DownloadDir: "before",
},
},
{
"Merge DownloadDir Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
DownloadDir: "after",
},
},
&DownloaderStoreConfig{
DownloadDir: "after",
},
},
{
"Merge MaxRunning No Override",
&DownloaderStoreConfig{
MaxRunning: 1,
},
args{
beforeCfg: &DownloaderStoreConfig{
MaxRunning: 10,
},
},
&DownloaderStoreConfig{
MaxRunning: 1,
},
},
{
"Merge MaxRunning Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
MaxRunning: 10,
},
},
&DownloaderStoreConfig{
MaxRunning: 10,
},
},
{
"Merge ProtocolConfig No Override",
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{},
},
args{
beforeCfg: &DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{},
},
},
{
"Merge ProtocolConfig Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "after",
},
},
},
{
"Merge Extra No Override",
&DownloaderStoreConfig{
Extra: map[string]any{},
},
args{
beforeCfg: &DownloaderStoreConfig{
Extra: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
Extra: map[string]any{},
},
},
{
"Merge Extra Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
Extra: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
Extra: map[string]any{
"key": "after",
},
},
},
{
"Merge Proxy No Override",
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{},
},
args{
beforeCfg: &DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Scheme: "http",
},
},
},
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{},
},
},
{
"Merge Proxy Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Scheme: "http",
},
},
},
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Scheme: "http",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &DownloaderStoreConfig{
FirstLoad: tt.fields.FirstLoad,
DownloadDir: tt.fields.DownloadDir,
MaxRunning: tt.fields.MaxRunning,
ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
}
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Merge() = %v, want %v", got, tt.want)
}
})
}
}
+366 -159
View File
File diff suppressed because it is too large. Load diff
+273 -20
View File
@@ -5,6 +5,8 @@ import (
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"os" "os"
"strconv"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -80,7 +82,45 @@ func TestDownloader_Create(t *testing.T) {
want := test.FileMd5(test.BuildFile) want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile) got := test.FileMd5(test.DownloadFile)
if want != got { if want != got {
t.Errorf("Download() got = %v, want %v", got, want) t.Errorf("Downloader_Create() got = %v, want %v", got, want)
}
}
func TestDownloader_CreateNotInWhite(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
downloader := NewDownloader(&DownloaderConfig{
DownloadDirWhiteList: []string{"./downloads"},
})
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
rr, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
wg.Add(1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyDone {
wg.Done()
}
})
_, err = downloader.Create(rr.ID, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
})
if !strings.Contains(err.Error(), "white") {
t.Errorf("TestDownloader_CreateNotInWhite() got = %v, want %v", err.Error(), "not in white list")
} }
} }
@@ -92,15 +132,33 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
if err := downloader.Setup(); err != nil { if err := downloader.Setup(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer downloader.Clear() defer func() {
downloader.Delete(nil, true)
downloader.Clear()
}()
reqs := make([]*base.Request, 0) reqs := make([]*base.Request, 0)
for i := 0; i < 6; i++ { fileNames := make([]string, 0)
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, req)
if i == 0 {
fileNames = append(fileNames, test.DownloadName)
} else {
arr := strings.Split(test.DownloadName, ".")
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
} }
}
var wg sync.WaitGroup
wg.Add(len(reqs))
downloader.Listener(func(event *Event) {
if event.Key == EventKeyDone {
wg.Done()
}
})
_, err := downloader.CreateDirectBatch(reqs, &base.Options{ _, err := downloader.CreateDirectBatch(reqs, &base.Options{
Path: test.Dir, Path: test.Dir,
@@ -113,29 +171,47 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
wg.Wait()
tasks := downloader.GetTasks() tasks := downloader.GetTasks()
if len(tasks) != len(reqs) { if len(tasks) != len(reqs) {
t.Errorf("CreateDirectBatch() got = %v, want %v", len(tasks), len(reqs)) t.Errorf("CreateDirectBatch() task got = %v, want %v", len(tasks), len(reqs))
} }
nameMatchMap := make(map[string]bool)
for _, name := range fileNames {
if _, err := os.Stat(test.Dir + "/" + name); os.IsNotExist(err) {
t.Errorf("CreateDirectBatch() file not exist: %v", name)
}
for _, task := range tasks {
if name == task.Meta.Opts.Name {
nameMatchMap[task.Meta.Opts.Name] = true
}
}
}
if len(nameMatchMap) != len(reqs) {
t.Errorf("CreateDirectBatch() names got = %v, want %v", len(nameMatchMap), len(reqs))
}
} }
func TestDownloader_CreateWithProxy(t *testing.T) { func TestDownloader_CreateWithProxy(t *testing.T) {
// No proxy // No proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return nil return nil
}, nil) }, nil)
// Disable proxy // Disable proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Enable = false proxyCfg.Enable = false
return proxyCfg return proxyCfg
}, nil) }, nil)
// Enable system proxy but not set proxy environment variable // Enable system proxy but not set proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.System = true proxyCfg.System = true
return proxyCfg return proxyCfg
}, nil) }, nil)
// Enable proxy but error proxy environment variable // Enable proxy but error proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234") os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234") os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
proxyCfg.System = true proxyCfg.System = true
@@ -146,33 +222,50 @@ func TestDownloader_CreateWithProxy(t *testing.T) {
} }
}) })
// Enable system proxy and set proxy environment variable // Enable system proxy and set proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String()) os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String()) os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
proxyCfg.System = true proxyCfg.System = true
return proxyCfg return proxyCfg
}, nil) }, nil)
// Invalid proxy scheme // Invalid proxy scheme
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Scheme = "" proxyCfg.Scheme = ""
return proxyCfg return proxyCfg
}, nil) }, nil)
// Invalid proxy host // Invalid proxy host
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Host = "" proxyCfg.Host = ""
return proxyCfg return proxyCfg
}, nil) }, nil)
// Use proxy without auth // Use proxy without auth
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return proxyCfg return proxyCfg
}, nil) }, nil)
// Use proxy with auth // Use proxy with auth
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return proxyCfg return proxyCfg
}, nil) }, nil)
// Request proxy mode follow
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
reqProxy.Mode = base.RequestProxyModeFollow
return reqProxy
}, nil, nil)
// Request proxy mode none
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
reqProxy.Mode = base.RequestProxyModeNone
return reqProxy
}, nil, nil)
// Request proxy mode custom
doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy {
return reqProxy
}, nil, nil)
} }
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) { func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
usr, pwd := "", "" usr, pwd := "", ""
if auth { if auth {
usr, pwd = "admin", "123" usr, pwd = "admin", "123"
@@ -185,17 +278,29 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
t.Fatal(err) t.Fatal(err)
} }
defer downloader.Clear() defer downloader.Clear()
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&base.DownloaderProxyConfig{ globalProxyCfg := &base.DownloaderProxyConfig{
Enable: true, Enable: true,
Scheme: "socks5", Scheme: "socks5",
Host: proxyListener.Addr().String(), Host: proxyListener.Addr().String(),
Usr: usr, Usr: usr,
Pwd: pwd, Pwd: pwd,
}) }
if buildProxyConfig != nil {
globalProxyCfg = buildProxyConfig(globalProxyCfg)
}
downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg
req := &base.Request{ req := &base.Request{
URL: test.ExternalDownloadUrl, URL: test.ExternalDownloadUrl,
} }
if buildReqProxy != nil {
req.Proxy = buildReqProxy(&base.RequestProxy{
Scheme: "socks5",
Host: proxyListener.Addr().String(),
Usr: usr,
Pwd: pwd,
})
}
rr, err := downloader.Resolve(req) rr, err := downloader.Resolve(req)
if err != nil { if err != nil {
if errHandler == nil { if errHandler == nil {
@@ -295,7 +400,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(time.Millisecond * 1001) time.Sleep(time.Millisecond * 1001)
err = downloader.Pause(id) err = downloader.Pause(&TaskFilter{IDs: []string{id}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -319,7 +424,7 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
wg.Done() wg.Done()
} }
}) })
err = downloader.Continue(id) err = downloader.Continue(&TaskFilter{IDs: []string{id}})
wg.Wait() wg.Wait()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -342,8 +447,8 @@ func TestDownloader_Protocol_Config(t *testing.T) {
var httpCfg map[string]any var httpCfg map[string]any
exits := downloader.getProtocolConfig("http", &httpCfg) exits := downloader.getProtocolConfig("http", &httpCfg)
if exits { if !exits {
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false) t.Errorf("getProtocolConfig() got = %v, want %v", exits, true)
} }
storeCfg := &base.DownloaderStoreConfig{ storeCfg := &base.DownloaderStoreConfig{
@@ -380,3 +485,151 @@ func TestDownloader_Protocol_Config(t *testing.T) {
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg)) t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
} }
} }
func TestDownloader_GetTasksByFilter(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer func() {
downloader.Delete(nil, true)
downloader.Clear()
}()
reqs := make([]*base.Request, 0)
fileNames := make([]string, 0)
for i := 0; i < 10; i++ {
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
reqs = append(reqs, req)
if i == 0 {
fileNames = append(fileNames, test.DownloadName)
} else {
arr := strings.Split(test.DownloadName, ".")
fileNames = append(fileNames, arr[0]+" ("+strconv.Itoa(i)+")."+arr[1])
}
}
var wg sync.WaitGroup
wg.Add(len(reqs))
downloader.Listener(func(event *Event) {
if event.Key == EventKeyDone {
wg.Done()
}
})
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
})
if err != nil {
t.Fatal(err)
}
wg.Wait()
t.Run("GetTasksByFilter nil", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(nil)
if len(tasks) != len(reqs) {
t.Errorf("GetTasksByFilter nil task got = %v, want %v", len(tasks), len(reqs))
}
})
t.Run("GetTasksByFilter empty", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{})
if len(tasks) != len(reqs) {
t.Errorf("GetTasksByFilter empty task got = %v, want %v", len(tasks), len(reqs))
}
})
t.Run("GetTasksByFilter ids", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
IDs: taskIds,
})
if len(tasks) != len(reqs) {
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), len(reqs))
}
})
t.Run("GetTasksByFilter match ids", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
IDs: []string{taskIds[0]},
})
if len(tasks) != 1 {
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 1)
}
})
t.Run("GetTasksByFilter not match ids", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
IDs: []string{"xxx"},
})
if len(tasks) != 0 {
t.Errorf("GetTasksByFilter ids task got = %v, want %v", len(tasks), 0)
}
})
t.Run("GetTasksByFilter status", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
Statuses: []base.Status{base.DownloadStatusDone},
})
if len(tasks) != len(reqs) {
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), len(reqs))
}
})
t.Run("GetTasksByFilter not match status", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
Statuses: []base.Status{base.DownloadStatusError},
})
if len(tasks) != 0 {
t.Errorf("GetTasksByFilter status task got = %v, want %v", len(tasks), 0)
}
})
t.Run("GetTasksByFilter match notStatus", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
NotStatuses: []base.Status{base.DownloadStatusRunning, base.DownloadStatusPause},
})
if len(tasks) != len(reqs) {
t.Errorf("GetTasksByFilter match notStatus task got = %v, want %v", len(tasks), len(reqs))
}
})
t.Run("GetTasksByFilter not match notStatus", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
NotStatuses: []base.Status{base.DownloadStatusDone},
})
if len(tasks) != 0 {
t.Errorf("GetTasksByFilter not match notStatus task got = %v, want %v", len(tasks), 0)
}
})
t.Run("GetTasksByFilter match ids and status", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
IDs: []string{taskIds[0]},
Statuses: []base.Status{base.DownloadStatusDone},
})
if len(tasks) != 1 {
t.Errorf("GetTasksByFilter match ids and status task got = %v, want %v", len(tasks), 1)
}
})
t.Run("GetTasksByFilter not match ids and status", func(t *testing.T) {
tasks := downloader.GetTasksByFilter(&TaskFilter{
IDs: []string{taskIds[0]},
Statuses: []base.Status{base.DownloadStatusError},
})
if len(tasks) != 0 {
t.Errorf("GetTasksByFilter not match ids and status task got = %v, want %v", len(tasks), 0)
}
})
}
+10 -2
View File
@@ -106,7 +106,7 @@ func (e *Engine) await(value any) {
} }
func (e *Engine) Close() { func (e *Engine) Close() {
e.loop.Stop() e.loop.StopNoWait()
} }
type Config struct { type Config struct {
@@ -178,7 +178,15 @@ func resolveResult(value goja.Value) (any, error) {
if err, ok := p.Result().Export().(error); ok { if err, ok := p.Result().Export().(error); ok {
return nil, err return nil, err
} else { } else {
return nil, errors.New(p.Result().String()) stack := p.Result().String()
result := p.Result()
if ro, ok := result.(*goja.Object); ok {
stackVal := ro.Get("stack")
if stackVal != nil && stackVal.String() != "" {
stack = stackVal.String()
}
}
return nil, errors.New(stack)
} }
} }
} }
+71 -6
View File
@@ -6,12 +6,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -19,6 +13,13 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
) )
func TestPolyfill(t *testing.T) { func TestPolyfill(t *testing.T) {
@@ -26,6 +27,8 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "XMLHttpRequest") doTestPolyfill(t, "XMLHttpRequest")
doTestPolyfill(t, "Blob") doTestPolyfill(t, "Blob")
doTestPolyfill(t, "FormData") doTestPolyfill(t, "FormData")
doTestPolyfill(t, "TextDecoder")
doTestPolyfill(t, "TextEncoder")
doTestPolyfill(t, "fetch") doTestPolyfill(t, "fetch")
doTestPolyfill(t, "__gopeed_create_vm") doTestPolyfill(t, "__gopeed_create_vm")
} }
@@ -69,6 +72,44 @@ async function testOctetStream(file){
return await resp.text(); return await resp.text();
} }
async function testRedirect() {
const url = host + '/redirect?num=3'
return await new Promise((resolve, reject) => {
fetch(url, {
method: 'HEAD',
redirect: 'error',
}).then(()=>reject())
fetch(url, {
method: 'HEAD',
redirect: 'follow',
}).then((res) =>res.headers.has('location') && reject()).catch(() => reject())
fetch(url, {
method: 'HEAD',
redirect: 'manual',
}).then((res) => {
const location = res.headers.get('location');
location ? resolve(location) : reject()
}).catch(() => reject())
})
}
async function testResponseUrl() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', host+'/redirect?num=3');
xhr.onload = function(){
if (xhr.responseURL.includes('/redirect?num=0')){
resolve();
}else{
reject();
}
};
xhr.send();
});
}
async function testFormData(file){ async function testFormData(file){
const formData = new FormData(); const formData = new FormData();
formData.append('name', 'test'); formData.append('name', 'test');
@@ -196,6 +237,20 @@ function testTimeout(){
} }
}() }()
t.Run("testRedirect", func(t *testing.T) {
_, err := callTestFun(engine, "testRedirect")
if err != nil {
t.Fatal(err)
}
})
t.Run("testResponseUrl", func(t *testing.T) {
_, err = callTestFun(engine, "testResponseUrl")
if err != nil {
t.Fatal(err)
}
})
func() { func() {
jsFile, goFile, md5 := buildFile(t, engine.Runtime) jsFile, goFile, md5 := buildFile(t, engine.Runtime)
result, err = callTestFun(engine, "testFormData", jsFile) result, err = callTestFun(engine, "testFormData", jsFile)
@@ -405,6 +460,16 @@ func startServer() net.Listener {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte("ok")) w.Write([]byte("ok"))
}) })
mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
num := r.URL.Query().Get("num")
n, _ := strconv.Atoi(num)
if n > 0 {
http.Redirect(w, r, fmt.Sprintf("/redirect?num=%d", n-1), http.StatusFound)
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
})
server.Handler = mux server.Handler = mux
go server.Serve(listener) go server.Serve(listener)
return listener return listener
+55 -22
View File
@@ -3,10 +3,6 @@ package xhr
import ( import (
"bytes" "bytes"
"errors" "errors"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
"io" "io"
"mime/multipart" "mime/multipart"
"net" "net"
@@ -14,6 +10,11 @@ import (
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
) )
const ( const (
@@ -25,6 +26,12 @@ const (
eventTimeout = "timeout" eventTimeout = "timeout"
) )
const (
redirectError = "error"
redirectFollow = "follow"
redirectManual = "manual"
)
type ProgressEvent struct { type ProgressEvent struct {
Type string `json:"type"` Type string `json:"type"`
LengthComputable bool `json:"lengthComputable"` LengthComputable bool `json:"lengthComputable"`
@@ -124,6 +131,7 @@ type XMLHttpRequest struct {
aborted bool aborted bool
proxyHandler func(r *http.Request) (*url.URL, error) proxyHandler func(r *http.Request) (*url.URL, error)
WithCredentials bool `json:"withCredentials"`
Upload *XMLHttpRequestUpload `json:"upload"` Upload *XMLHttpRequestUpload `json:"upload"`
Timeout int `json:"timeout"` Timeout int `json:"timeout"`
ReadyState int `json:"readyState"` ReadyState int `json:"readyState"`
@@ -131,6 +139,13 @@ type XMLHttpRequest struct {
StatusText string `json:"statusText"` StatusText string `json:"statusText"`
Response string `json:"response"` Response string `json:"response"`
ResponseText string `json:"responseText"` ResponseText string `json:"responseText"`
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseURL
// https://xhr.spec.whatwg.org/#the-responseurl-attribute
ResponseUrl string `json:"responseURL"`
// extend fetch redirect
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#redirect
// https://fetch.spec.whatwg.org/#concept-request-redirect-mode
Redirect string `json:"redirect"`
*EventProp *EventProp
Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"` Onreadystatechange func(event *ProgressEvent) `json:"onreadystatechange"`
} }
@@ -154,31 +169,33 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
var ( var (
contentType string contentType string
contentLength int64 contentLength int64
isStringBody bool
) )
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" { if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
req, err = http.NewRequest(xhr.method, xhr.url, nil) req, err = http.NewRequest(xhr.method, xhr.url, nil)
} else { } else {
switch d.(type) { switch v := d.(type) {
case string: case string:
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(d.(string))) req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
contentType = "text/plain;charset=UTF-8" contentType = "text/plain;charset=UTF-8"
contentLength = int64(len(d.(string))) contentLength = int64(len(v))
isStringBody = true
case *file.File: case *file.File:
req, err = http.NewRequest(xhr.method, xhr.url, d.(*file.File).Reader) req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
contentType = "application/octet-stream" contentType = "application/octet-stream"
contentLength = d.(*file.File).Size contentLength = v.Size
case *formdata.FormData: case *formdata.FormData:
pr, pw := io.Pipe() pr, pw := io.Pipe()
mw := NewMultipart(pw) mw := NewMultipart(pw)
for _, e := range d.(*formdata.FormData).Entries() { for _, e := range v.Entries() {
arr := e.([]any) arr := e.([]any)
k := arr[0].(string) k := arr[0].(string)
v := arr[1] v := arr[1]
switch v.(type) { switch v := v.(type) {
case string: case string:
mw.WriteField(k, v.(string)) mw.WriteField(k, v)
case *file.File: case *file.File:
mw.WriteFile(k, v.(*file.File)) mw.WriteFile(k, v)
} }
} }
go func() { go func() {
@@ -196,7 +213,8 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
return return
} }
req.Header = xhr.requestHeaders req.Header = xhr.requestHeaders
if contentType != "" { // Only string body can specify Content-Type header by user
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
} }
if contentLength > 0 { if contentLength > 0 {
@@ -208,6 +226,18 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
client := &http.Client{ client := &http.Client{
Transport: transport, Transport: transport,
Timeout: time.Duration(xhr.Timeout) * time.Millisecond, Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if xhr.Redirect == redirectManual {
return http.ErrUseLastResponse
}
if xhr.Redirect == redirectError {
return errors.New("redirect failed")
}
if len(via) > 20 {
return errors.New("too many redirects")
}
return nil
},
} }
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@@ -229,6 +259,11 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
if !xhr.aborted { if !xhr.aborted {
xhr.Upload.callOnload() xhr.Upload.callOnload()
} }
responseUrl := resp.Request.URL
responseUrl.Fragment = ""
xhr.ResponseUrl = responseUrl.String()
xhr.responseHeaders = resp.Header xhr.responseHeaders = resp.Header
xhr.Status = resp.StatusCode xhr.Status = resp.StatusCode
xhr.StatusText = resp.Status xhr.StatusText = resp.Status
@@ -384,10 +419,9 @@ func (w *multipartWrapper) Size() int64 {
w.statWriter.Close() w.statWriter.Close()
size := int64(w.statBuffer.Len()) size := int64(w.statBuffer.Len())
for _, v := range w.fields { for _, v := range w.fields {
switch v.(type) { switch v := v.(type) {
case *file.File: case *file.File:
f := v.(*file.File) size += v.Size
size += f.Size
} }
} }
return size return size
@@ -395,18 +429,17 @@ func (w *multipartWrapper) Size() int64 {
func (w *multipartWrapper) Send() error { func (w *multipartWrapper) Send() error {
for k, v := range w.fields { for k, v := range w.fields {
switch v.(type) { switch v := v.(type) {
case string: case string:
if err := w.writer.WriteField(k, v.(string)); err != nil { if err := w.writer.WriteField(k, v); err != nil {
return err return err
} }
case *file.File: case *file.File:
f := v.(*file.File) fw, err := w.writer.CreateFormFile(k, v.Name)
fw, err := w.writer.CreateFormFile(k, f.Name)
if err != nil { if err != nil {
return err return err
} }
if _, err = io.Copy(fw, f); err != nil { if _, err = io.Copy(fw, v); err != nil {
return err return err
} }
} }
+1 -1
View File
@@ -1 +1 @@
(()=>{var t={725:function(t,e,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(t){"use strict";var e=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(t,e){return(e=document.createElement("a")).href=t,e},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=e&&e.prototype.append&&e.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(t){}function c(t){return t.map((function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var r=new Uint8Array(t.byteLength);r.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=r.buffer}return e}return t}))}function h(t,r){r=r||{};var o=new e;return c(t).forEach((function(t){o.append(t)})),r.type?o.getBlob(r.type):o.getBlob()}function p(t,e){return new o(c(t),e||{})}i.Blob&&(h.prototype=Blob.prototype,p.prototype=Blob.prototype);var y="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(t){for(var e=0,r=t.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);e<r;){var u=t.charCodeAt(e++);if(u>=55296&&u<=56319){if(e<r){var f=t.charCodeAt(e);56320==(64512&f)&&(++e,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+e/t.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},d="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(t){for(var e=t.length,r=[],o=0;o<e;){var n,i,a,s,u=t[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=e)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=t[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=t[o+1],i=t[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=t[o+1],i=t[o+2],a=t[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",p=0;p<c;)h+=String.fromCharCode.apply(String,r.slice(p,p+=4096));return h};function b(){var e=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;e&&r&&(XMLHttpRequest.prototype.send=function(t){t instanceof Blob?(this.setRequestHeader("Content-Type",t.type),r.call(this,t)):r.call(this,t)});try{new File([],""),t.File=i.File,t.FileReader=i.FileReader}catch(e){try{t.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(e){t.File=function(t,e,r){var o=new Blob(t,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=e.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),t.Blob=f?i.Blob:p):l?(b(),t.Blob=h):function(){function e(t){for(var e=new Array(t.byteLength),r=new Uint8Array(t),o=e.length;o--;)e[o]=r[o];return e}function o(t){for(var e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<t.length;o+=3){var n=t[o],i=o+1<t.length,a=i?t[o+1]:0,s=o+2<t.length,u=s?t[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(e[f],e[l],e[c],e[h])}return r.join("")}var s=Object.create||function(t){function e(){}return e.prototype=t,new e};function u(t){return Object.prototype.toString.call(t).slice(8,-1)}function f(t,e){return"object"==typeof t&&Object.prototype.isPrototypeOf.call(t.prototype,e)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(t){return e=l,r=u(t),-1!==e.indexOf(r)||f(i.ArrayBuffer,t);var e,r}function h(t,r){r=null==r?{}:r;for(var o=0,n=(t=t?t.slice():[]).length;o<n;o++){var a=t[o];a instanceof h?t[o]=a._buffer:"string"==typeof a?t[o]=y(a):"DataView"===u(s=a)||f(i.DataView,s)?t[o]=e(a.buffer):c(a)?t[o]=e(a):t[o]=y(String(a))}var s;this._buffer=i.Uint8Array?function(t){for(var e=0,r=t.length;r--;)e+=t[r].length;for(var o=new Uint8Array(e),n=0,i=0;i<t.length;i++){var a=t[i];o.set(a,n),n+=a.byteLength||a.length}return o}(t):[].concat.apply([],t),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated (()=>{var e={725:function(e,t,r){var o,n,i;i="undefined"!=typeof self&&self||"undefined"!=typeof window&&window||void 0!==r.g&&r.g||this,o=function(e){"use strict";var t=i.BlobBuilder||i.WebKitBlobBuilder||i.MSBlobBuilder||i.MozBlobBuilder,r=i.URL||i.webkitURL||function(e,t){return(t=document.createElement("a")).href=e,t},o=i.Blob,n=r.createObjectURL,a=r.revokeObjectURL,s=i.Symbol&&i.Symbol.toStringTag,u=!1,f=!1,l=t&&t.prototype.append&&t.prototype.getBlob;try{u=2===new Blob(["ä"]).size,f=2===new Blob([new Uint8Array([1,2])]).size}catch(e){}function c(e){return e.map((function(e){if(e.buffer instanceof ArrayBuffer){var t=e.buffer;if(e.byteLength!==t.byteLength){var r=new Uint8Array(e.byteLength);r.set(new Uint8Array(t,e.byteOffset,e.byteLength)),t=r.buffer}return t}return e}))}function h(e,r){r=r||{};var o=new t;return c(e).forEach((function(e){o.append(e)})),r.type?o.getBlob(r.type):o.getBlob()}function d(e,t){return new o(c(e),t||{})}i.Blob&&(h.prototype=Blob.prototype,d.prototype=Blob.prototype);var p="function"==typeof TextEncoder?TextEncoder.prototype.encode.bind(new TextEncoder):function(e){for(var t=0,r=e.length,o=i.Uint8Array||Array,n=0,a=Math.max(32,r+(r>>1)+7),s=new o(a>>3<<3);t<r;){var u=e.charCodeAt(t++);if(u>=55296&&u<=56319){if(t<r){var f=e.charCodeAt(t);56320==(64512&f)&&(++t,u=((1023&u)<<10)+(1023&f)+65536)}if(u>=55296&&u<=56319)continue}if(n+4>s.length){a+=8,a=(a*=1+t/e.length*2)>>3<<3;var l=new Uint8Array(a);l.set(s),s=l}if(0!=(4294967168&u)){if(0==(4294965248&u))s[n++]=u>>6&31|192;else if(0==(4294901760&u))s[n++]=u>>12&15|224,s[n++]=u>>6&63|128;else{if(0!=(4292870144&u))continue;s[n++]=u>>18&7|240,s[n++]=u>>12&63|128,s[n++]=u>>6&63|128}s[n++]=63&u|128}else s[n++]=u}return s.slice(0,n)},y="function"==typeof TextDecoder?TextDecoder.prototype.decode.bind(new TextDecoder):function(e){for(var t=e.length,r=[],o=0;o<t;){var n,i,a,s,u=e[o],f=null,l=u>239?4:u>223?3:u>191?2:1;if(o+l<=t)switch(l){case 1:u<128&&(f=u);break;case 2:128==(192&(n=e[o+1]))&&(s=(31&u)<<6|63&n)>127&&(f=s);break;case 3:n=e[o+1],i=e[o+2],128==(192&n)&&128==(192&i)&&(s=(15&u)<<12|(63&n)<<6|63&i)>2047&&(s<55296||s>57343)&&(f=s);break;case 4:n=e[o+1],i=e[o+2],a=e[o+3],128==(192&n)&&128==(192&i)&&128==(192&a)&&(s=(15&u)<<18|(63&n)<<12|(63&i)<<6|63&a)>65535&&s<1114112&&(f=s)}null===f?(f=65533,l=1):f>65535&&(f-=65536,r.push(f>>>10&1023|55296),f=56320|1023&f),r.push(f),o+=l}for(var c=r.length,h="",d=0;d<c;)h+=String.fromCharCode.apply(String,r.slice(d,d+=4096));return h};function b(){var t=!!i.ActiveXObject||"-ms-scroll-limit"in document.documentElement.style&&"-ms-ime-align"in document.documentElement.style,r=i.XMLHttpRequest&&i.XMLHttpRequest.prototype.send;t&&r&&(XMLHttpRequest.prototype.send=function(e){e instanceof Blob?(this.setRequestHeader("Content-Type",e.type),r.call(this,e)):r.call(this,e)});try{new File([],""),e.File=i.File,e.FileReader=i.FileReader}catch(t){try{e.File=new Function('class File extends Blob {constructor(chunks, name, opts) {opts = opts || {};super(chunks, opts || {});this.name = name.replace(/\\//g, ":");this.lastModifiedDate = opts.lastModified ? new Date(opts.lastModified) : new Date();this.lastModified = +this.lastModifiedDate;}};return new File([], ""), File')()}catch(t){e.File=function(e,t,r){var o=new Blob(e,r),n=r&&void 0!==r.lastModified?new Date(r.lastModified):new Date;return o.name=t.replace(/\//g,":"),o.lastModifiedDate=n,o.lastModified=+n,o.toString=function(){return"[object File]"},s&&(o[s]="File"),o}}}}u?(b(),e.Blob=f?i.Blob:d):l?(b(),e.Blob=h):function(){function t(e){for(var t=new Array(e.byteLength),r=new Uint8Array(e),o=t.length;o--;)t[o]=r[o];return t}function o(e){for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",r=[],o=0;o<e.length;o+=3){var n=e[o],i=o+1<e.length,a=i?e[o+1]:0,s=o+2<e.length,u=s?e[o+2]:0,f=n>>2,l=(3&n)<<4|a>>4,c=(15&a)<<2|u>>6,h=63&u;s||(h=64,i||(c=64)),r.push(t[f],t[l],t[c],t[h])}return r.join("")}var s=Object.create||function(e){function t(){}return t.prototype=e,new t};function u(e){return Object.prototype.toString.call(e).slice(8,-1)}function f(e,t){return"object"==typeof e&&Object.prototype.isPrototypeOf.call(e.prototype,t)}var l=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","ArrayBuffer"];function c(e){return t=l,r=u(e),-1!==t.indexOf(r)||f(i.ArrayBuffer,e);var t,r}function h(e,r){r=null==r?{}:r;for(var o=0,n=(e=e?e.slice():[]).length;o<n;o++){var a=e[o];a instanceof h?e[o]=a._buffer:"string"==typeof a?e[o]=p(a):"DataView"===u(s=a)||f(i.DataView,s)?e[o]=t(a.buffer):c(a)?e[o]=t(a):e[o]=p(String(a))}var s;this._buffer=i.Uint8Array?function(e){for(var t=0,r=e.length;r--;)t+=e[r].length;for(var o=new Uint8Array(t),n=0,i=0;i<e.length;i++){var a=e[i];o.set(a,n),n+=a.byteLength||a.length}return o}(e):[].concat.apply([],e),this.size=this._buffer.length,this.type=r.type||"",/[^\u0020-\u007E]/.test(this.type)?this.type="":this.type=this.type.toLowerCase()}funcLine truncated
File diff suppressed because it is too large. Load diff
+5 -2
View File
@@ -7,16 +7,19 @@
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode production --watch", "dev": "webpack --mode production --watch",
"build": "webpack --mode production" "build": "webpack --mode production",
"postinstall": "patch-package"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"patch-package": "^8.0.0",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
}, },
"dependencies": { "dependencies": {
"blob-polyfill": "^7.0.20220408", "blob-polyfill": "^7.0.20220408",
"whatwg-fetch": "^3.6.2" "fastestsmallesttextencoderdecoder": "^1.0.22",
"whatwg-fetch": "^3.6.20"
} }
} }
@@ -0,0 +1,46 @@
diff --git a/node_modules/whatwg-fetch/dist/fetch.umd.js b/node_modules/whatwg-fetch/dist/fetch.umd.js
index 7a0d852..604691e 100644
--- a/node_modules/whatwg-fetch/dist/fetch.umd.js
+++ b/node_modules/whatwg-fetch/dist/fetch.umd.js
@@ -394,6 +394,7 @@
}
}());
this.referrer = null;
+ this.redirect = options.redirect || 'follow'
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
@@ -606,6 +607,10 @@
}
}
+ if ('redirect' in xhr) {
+ xhr.redirect = request.redirect
+ }
+
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
diff --git a/node_modules/whatwg-fetch/fetch.js b/node_modules/whatwg-fetch/fetch.js
index f39a983..d1fc903 100644
--- a/node_modules/whatwg-fetch/fetch.js
+++ b/node_modules/whatwg-fetch/fetch.js
@@ -388,6 +388,7 @@ export function Request(input, options) {
}
}());
this.referrer = null
+ this.redirect = options.redirect || 'follow'
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
throw new TypeError('Body not allowed for GET or HEAD requests')
@@ -600,6 +601,10 @@ export function fetch(input, init) {
}
}
+ if ('redirect' in xhr) {
+ xhr.redirect = request.redirect
+ }
+
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
var names = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
@@ -1,3 +1,5 @@
import "./blob/index.js" import "./blob/index.js"
import "./crypto/index.js" import "./crypto/index.js"
// polyfill TextEncoder
import 'fastestsmallesttextencoderdecoder';
import "./fetch/index.js" import "./fetch/index.js"
+41 -10
View File
@@ -38,7 +38,7 @@ const (
EventOnResolve ActivationEvent = "onResolve" EventOnResolve ActivationEvent = "onResolve"
EventOnStart ActivationEvent = "onStart" EventOnStart ActivationEvent = "onStart"
EventOnError ActivationEvent = "onError" EventOnError ActivationEvent = "onError"
//EventOnDone ActivationEvent = "onDone" EventOnDone ActivationEvent = "onDone"
) )
func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) { func (d *Downloader) InstallExtensionByGit(url string) (*Extension, error) {
@@ -155,7 +155,13 @@ func (d *Downloader) DeleteExtension(identity string) error {
break break
} }
} }
return d.storage.Delete(bucketExtension, identity) if err = d.storage.Delete(bucketExtension, identity); err != nil {
return err
}
if err = d.storage.Delete(bucketExtensionStorage, identity); err != nil {
return err
}
return nil
} }
func (d *Downloader) GetExtensions() []*Extension { func (d *Downloader) GetExtensions() []*Extension {
@@ -298,6 +304,17 @@ func (d *Downloader) triggerOnError(task *Task, err error) {
) )
} }
func (d *Downloader) triggerOnDone(task *Task) {
doTrigger(d,
EventOnDone,
task.Meta.Req,
&OnErrorContext{
Task: NewExtensionTask(d, task),
},
nil,
)
}
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error { func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
// init extension global object // init extension global object
gopeed := &Instance{ gopeed := &Instance{
@@ -605,9 +622,9 @@ func (h InstanceEvents) OnError(fn goja.Callable) {
h.register(EventOnError, fn) h.register(EventOnError, fn)
} }
//func (h InstanceEvents) OnDone(fn goja.Callable) { func (h InstanceEvents) OnDone(fn goja.Callable) {
// h.register(HookEventOnDone, fn) h.register(EventOnDone, fn)
//} }
type ExtensionInfo struct { type ExtensionInfo struct {
Identity string `json:"identity"` Identity string `json:"identity"`
@@ -681,25 +698,39 @@ type OnErrorContext struct {
Error error `json:"error"` Error error `json:"error"`
} }
type ExtensionTask struct { type OnDoneContext struct {
*Task Task *Task `json:"task"`
}
// ExtensionTask is a wrapper of Task, it's used to interact with extension scripts.
// Avoid extension scripts modifying task directly, use ExtensionTask to encapsulate task,
// only some fields can be modified, such as request info.
type ExtensionTask struct {
download *Downloader download *Downloader
*Task
} }
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask { func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
// restricts extension scripts to only modify request info
newTask := task.clone()
newTask.Meta.Req = task.Meta.Req
return &ExtensionTask{ return &ExtensionTask{
Task: task.clone(),
download: download, download: download,
Task: newTask,
} }
} }
func (t *ExtensionTask) Continue() error { func (t *ExtensionTask) Continue() error {
return t.download.Continue(t.ID) return t.download.Continue(&TaskFilter{
IDs: []string{t.ID},
})
} }
func (t *ExtensionTask) Pause() error { func (t *ExtensionTask) Pause() error {
return t.download.Pause(t.ID) return t.download.Pause(&TaskFilter{
IDs: []string{t.ID},
})
} }
func parseSettings(settings []*Setting) map[string]any { func parseSettings(settings []*Setting) map[string]any {
+40 -3
View File
@@ -238,7 +238,7 @@ func TestDownloader_Extension_OnError(t *testing.T) {
select { select {
case err = <-errCh: case err = <-errCh:
break break
case <-time.After(time.Second * 30): case <-time.After(time.Second * 10):
err = errors.New("timeout") err = errors.New("timeout")
} }
@@ -247,8 +247,45 @@ func TestDownloader_Extension_OnError(t *testing.T) {
} }
// extension on error modify url and continue download // extension on error modify url and continue download
task := downloader.GetTask(id) task := downloader.GetTask(id)
if task.Meta.Req.URL != "https://github.com" { if task.Status != base.DownloadStatusDone {
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL) t.Fatalf("except status is done, actual: %s", task.Status)
}
})
}
func TestDownloader_Extension_OnDone(t *testing.T) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_done", false); err != nil {
t.Fatal(err)
}
errCh := make(chan error, 1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyFinally {
errCh <- event.Err
}
})
id, err := downloader.CreateDirect(&base.Request{
URL: "https://github.com",
}, nil)
if err != nil {
t.Fatal(err)
}
select {
case err = <-errCh:
break
case <-time.After(time.Second * 10):
err = errors.New("timeout")
}
// wait for script execution
time.Sleep(time.Millisecond * 3000)
if err != nil {
panic("extension on done download error: " + err.Error())
}
// extension on error modify url and continue download
task := downloader.GetTask(id)
if task.Meta.Req.Labels["modified"] != "true" {
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
} }
if task.Status != base.DownloadStatusDone { if task.Status != base.DownloadStatusDone {
t.Fatalf("except status is done, actual: %s", task.Status) t.Fatalf("except status is done, actual: %s", task.Status)
Loaded 50 of 130 files, more files were not shown because too many files have changed in this diff. Show more