mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
724276ac69 | ||
|
|
a3f559c8e5 | ||
|
|
edd189a8a8 | ||
|
|
c970378fd7 | ||
|
|
8b61343aef | ||
|
|
2cb93a03e2 | ||
|
|
81dfc073a6 | ||
|
|
427921ccab | ||
|
|
f4876c8a0e | ||
|
|
35a3625b11 | ||
|
|
dc9b501e35 | ||
|
|
85fe92a378 | ||
|
|
983d9ad09e | ||
|
|
d9f757aaf0 | ||
|
|
cd409fcfb9 | ||
|
|
366244fa5d | ||
|
|
7a2e9b4fd1 | ||
|
|
203385d3f8 | ||
|
|
aaf73dec24 | ||
|
|
2523282a89 | ||
|
|
3384f5197a | ||
|
|
cd77665f02 | ||
|
|
a22b431150 | ||
|
|
eb7c9ad7f2 | ||
|
|
ea11fc38f1 | ||
|
|
1be16e9f2c | ||
|
|
1d86d14a20 | ||
|
|
5ad15cc4ac | ||
|
|
49604966fd | ||
|
|
bdb8ed177f | ||
|
|
2dc7048cba | ||
|
|
d3750464aa | ||
|
|
5c9bcd3731 | ||
|
|
bdda550b2c | ||
|
|
3308d966f0 | ||
|
|
18041ed9ec | ||
|
|
56b85e545e | ||
|
|
c5af916952 | ||
|
|
a1981328aa | ||
|
|
51244133e0 | ||
|
|
0c17093b1a | ||
|
|
6538b29b62 | ||
|
|
81e90e6b89 | ||
|
|
4f39296664 | ||
|
|
ece5b91dc2 | ||
|
|
d9f04ae928 | ||
|
|
e3ccecbd9c | ||
|
|
fb1c2cb060 | ||
|
|
9bff75464d | ||
|
|
863f759248 | ||
|
|
f89fb5e438 | ||
|
|
7ca1cc8c64 | ||
|
|
1f791c66e6 | ||
|
|
4d60c47d39 | ||
|
|
3fd3dd49d2 | ||
|
|
d119de7b98 | ||
|
|
69b359a9c9 | ||
|
|
4fbe13a1f0 | ||
|
|
3b50465708 | ||
|
|
473c58796c | ||
|
|
fab8f135f3 | ||
|
|
0fca7b6bc7 | ||
|
|
4700d0ba28 | ||
|
|
6e31ea089f | ||
|
|
4a2088f6fb | ||
|
|
17c8fb7e4d | ||
|
|
8c99bbc847 | ||
|
|
06c7fe7a22 | ||
|
|
8b3b5d4394 | ||
|
|
2ee73ab619 | ||
|
|
15413dbc95 | ||
|
|
c7c43fac80 | ||
|
|
4283bdfa5a | ||
|
|
328ba8592c | ||
|
|
a191a4140d | ||
|
|
3521837964 | ||
|
|
b30cd78f86 | ||
|
|
0583e4aaa6 | ||
|
|
a89c55b5c4 | ||
|
|
68c40d7f3f | ||
|
|
42a0b89d95 | ||
|
|
1860f534cd | ||
|
|
5c2d2ad7b4 | ||
|
|
db6352ada8 | ||
|
|
d1a5ed8b3d | ||
|
|
be9583be0f | ||
|
|
d2c70b077b | ||
|
|
53b3eea4fc | ||
|
|
54347f41b6 | ||
|
|
cea454b015 | ||
|
|
070db64ca9 | ||
|
|
6a8956b04d | ||
|
|
057504bb81 | ||
|
|
470b488824 | ||
|
|
c8c9df1ae8 | ||
|
|
33d787c74e | ||
|
|
67338a5e83 | ||
|
|
723697192d | ||
|
|
4294d07d6a | ||
|
|
3936fd786f | ||
|
|
45197e9a3b | ||
|
|
c9692e76e5 | ||
|
|
0a9e39ed75 | ||
|
|
1971f582b5 | ||
|
|
c62d6be3da | ||
|
|
7d5e503894 | ||
|
|
6c4066fde4 | ||
|
|
ffdf3e717f | ||
|
|
f66814b4ae | ||
|
|
0c3e201a51 | ||
|
|
c32a5d750f | ||
|
|
207c287c97 | ||
|
|
56ef25065e | ||
|
|
bd6d6be71f | ||
|
|
1d3a028bae | ||
|
|
27011fb96b | ||
|
|
c696b59c4b | ||
|
|
78b5b8a78b | ||
|
|
c81ca7f5cd | ||
|
|
e39d50fe09 | ||
|
|
086269f8bd | ||
|
|
00d2183961 | ||
|
|
5f914dc4c4 | ||
|
|
9bf204cf04 | ||
|
|
a816b9ccc8 | ||
|
|
d60d370463 | ||
|
|
84d054b494 | ||
|
|
1aa96e147f | ||
|
|
8c01e45780 | ||
|
|
8ad571aa08 | ||
|
|
39316e9b8e | ||
|
|
f778596390 | ||
|
|
c2cbc14492 | ||
|
|
62c22bc6b4 | ||
|
|
b89c7bc608 | ||
|
|
f2cd686563 | ||
|
|
ef5ee99f04 | ||
|
|
6d136604db | ||
|
|
38ff711ea9 | ||
|
|
a1a8abc25f | ||
|
|
abccbabc78 | ||
|
|
c26eb26b8f | ||
|
|
74beea6044 | ||
|
|
e548d7e0c9 | ||
|
|
a996d57914 | ||
|
|
f7dc60ad50 | ||
|
|
8425a96ac9 | ||
|
|
99f4e8c895 | ||
|
|
cc0f61b527 | ||
|
|
922ef969a6 | ||
|
|
3a542054fc | ||
|
|
895a8d2ee4 | ||
|
|
29d0f5ebcf | ||
|
|
31228e3edf | ||
|
|
31a32d7e14 | ||
|
|
65fd6fb489 | ||
|
|
953b878a89 | ||
|
|
ae4ca3da58 | ||
|
|
d09bd180af | ||
|
|
3d24e353c5 | ||
|
|
ce7489b0c0 | ||
|
|
2b5091ba21 | ||
|
|
ff32971aca | ||
|
|
f2af61f9aa | ||
|
|
d935b9d631 | ||
|
|
ae66d1d83d | ||
|
|
aa91ca1d50 |
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
.git
|
||||||
|
|
||||||
|
*.data
|
||||||
|
*.log
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
.github
|
||||||
|
_docs
|
||||||
|
_examples
|
||||||
|
bin
|
||||||
|
ui
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* @monkeyWie
|
||||||
@@ -11,14 +11,19 @@ categories:
|
|||||||
- 'fix'
|
- 'fix'
|
||||||
- 'bugfix'
|
- 'bugfix'
|
||||||
- 'bug'
|
- 'bug'
|
||||||
|
- title: '🔧 Performance Improvements'
|
||||||
|
labels:
|
||||||
|
- 'perf'
|
||||||
- title: '🧪 Tests'
|
- title: '🧪 Tests'
|
||||||
label: 'test'
|
label: 'test'
|
||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
label: 'chore'
|
label: 'chore'
|
||||||
- title: '📖 Document'
|
- title: '📖 Document'
|
||||||
label: 'doc'
|
label: 'docs'
|
||||||
- title: '🚀 CI/CD'
|
- title: '🚀 CI/CD'
|
||||||
label: 'ci'
|
label: 'ci'
|
||||||
|
- title: '🌎 Internationalization'
|
||||||
|
label: 'translation'
|
||||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||||
version-resolver:
|
version-resolver:
|
||||||
major:
|
major:
|
||||||
@@ -32,7 +37,7 @@ version-resolver:
|
|||||||
- 'patch'
|
- 'patch'
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
# Changes
|
# Release notes
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
|
|
||||||
|
|||||||
+164
-179
File diff suppressed because it is too large.
Load diff
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.23"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
+99
-12
@@ -27,8 +27,8 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.16.1"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
@@ -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 .
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
ui/flutter/dist/
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n
|
|||||||
You only need to add the corresponding language file in this directory.
|
You only need to add the corresponding language file in this directory.
|
||||||
|
|
||||||
Please refer to `en_us.dart` for translation.
|
Please refer to `en_us.dart` for translation.
|
||||||
|
words prefixed with `@` are not meant to be translated.
|
||||||
|
|
||||||
## flutter development
|
## flutter development
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Hướng dẫn đóng góp cho Gopeed
|
||||||
|
|
||||||
|
Trước tiên, cảm ơn bạn đã quan tâm đến việc đóng góp cho Gopeed. Hướng dẫn này sẽ giúp bạn tham gia
|
||||||
|
phát triển Gopeed một cách tốt hơn.
|
||||||
|
|
||||||
|
## Mô tả nhánh
|
||||||
|
|
||||||
|
Dự án này chỉ có một nhánh chính duy nhất, đó là nhánh `main`. Nếu bạn muốn tham gia vào
|
||||||
|
phát triển Gopeed, hãy fork dự án này trước, sau đó phát triển trong dự án fork của bạn. Sau khi
|
||||||
|
hoàn thành phát triển, gửi một PR đến dự án này và merge vào nhánh `main`.
|
||||||
|
|
||||||
|
## Phát triển cục bộ
|
||||||
|
|
||||||
|
Đề nghị phát triển và gỡ lỗi thông qua web. Đầu tiên, khởi động dịch vụ backend bằng cách chạy
|
||||||
|
lệnh `go run cmd/api/main.go` trong dòng lệnh, cổng mặc định của dịch vụ là `9999`, sau đó
|
||||||
|
khởi động dự án flutter frontend trong chế độ `debug` để chạy.
|
||||||
|
|
||||||
|
## Dịch thuật
|
||||||
|
|
||||||
|
Các tệp quốc tế hóa của Gopeed được đặt trong thư mục `ui/flutter/lib/i18n/langs`.
|
||||||
|
Bạn chỉ cần thêm tệp ngôn ngữ tương ứng trong thư mục này.
|
||||||
|
|
||||||
|
Vui lòng tham khảo `en_us.dart` để biết cách dịch thuật.
|
||||||
|
|
||||||
|
## Phát triển flutter
|
||||||
|
|
||||||
|
Đừng quên chạy `dart format ./ui/flutter` trước khi commit để giữ mã của bạn theo định dạng dart chuẩn.
|
||||||
|
|
||||||
|
Bật build_runner watcher nếu bạn muốn chỉnh sửa api/models:
|
||||||
@@ -16,7 +16,7 @@ flutter 项目即可运行。
|
|||||||
|
|
||||||
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
|
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
|
||||||
|
|
||||||
请注意以 `en_US.json` 为参照进行翻译。
|
请注意以 `en_us.dart` 为参照进行翻译。
|
||||||
|
|
||||||
## flutter开发
|
## flutter开发
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
|
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
|
||||||
|
|
||||||
|
|
||||||
請以 `en_US.json` 作為參照。
|
請以 `en_us.dart` 作為參照。
|
||||||
|
|
||||||
## flutter開發
|
## flutter開發
|
||||||
|
|
||||||
|
|||||||
+14
-12
@@ -1,21 +1,23 @@
|
|||||||
FROM instrumentisto/flutter:3.16.1 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.19.3 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
|
||||||
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/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" \
|
||||||
|
-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
|
||||||
|
RUN apk update && \
|
||||||
|
apk add --no-cache su-exec ; \
|
||||||
|
chmod +x ./entrypoint.sh && \
|
||||||
|
rm -rf /var/cache/apk/*
|
||||||
|
VOLUME ["/app/storage"]
|
||||||
|
ENV PUID=0 PGID=0 UMASK=022
|
||||||
EXPOSE 9999
|
EXPOSE 9999
|
||||||
ENTRYPOINT ["./gopeed"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,34 +7,84 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
|
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
|
||||||
|
|
||||||
|
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
|
||||||
|
|
||||||
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
|
## Download
|
||||||
|
|
||||||
## Install
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Platform</th>
|
||||||
|
<th>Package Type</th>
|
||||||
|
<th>Download Link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
**Supported platforms**
|
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||||
|
|
||||||
- [x] windows
|
|
||||||
- [x] macos
|
|
||||||
- [x] linux
|
|
||||||
- [x] android
|
|
||||||
- [x] ios
|
|
||||||
- [x] web
|
|
||||||
- [x] docker
|
|
||||||
|
|
||||||
[Download](https://github.com/GopeedLab/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal
|
|
||||||
|
|
||||||
### Command tool
|
### Command tool
|
||||||
|
|
||||||
@@ -42,23 +94,9 @@ use `go install`:
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
## Browser Extension
|
||||||
|
|
||||||
#### Start directly
|
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Using Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Access Gopeed
|
|
||||||
|
|
||||||
When the docker container is running, you can access the web page through `http://localhost:9999`.
|
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
@@ -76,8 +114,8 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|
||||||
@@ -136,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" -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
|
||||||
```
|
```
|
||||||
@@ -160,7 +198,6 @@ cd ../../
|
|||||||
rm -rf cmd/web/dist
|
rm -rf cmd/web/dist
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
go run cmd/web/main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|||||||
+71
-45
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,11 +7,11 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## はじめに
|
## はじめに
|
||||||
|
|
||||||
@@ -19,49 +21,74 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
|
|
||||||
## インストール
|
## インストール
|
||||||
|
|
||||||
**サポートするプラットフォーム**
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Platform</th>
|
||||||
|
<th>Package Type</th>
|
||||||
|
<th>Download Link</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
- [x] windows
|
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||||
- [x] macos
|
|
||||||
- [x] linux
|
|
||||||
- [x] android
|
|
||||||
- [x] ios
|
|
||||||
- [x] web
|
|
||||||
- [x] docker
|
|
||||||
|
|
||||||
[ダウンロード](https://github.com/GopeedLab/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> ヒント: macos を開くのに失敗した場合は、ターミナルで `xattr -d com.apple.quarantine /Applications/Gopeed.app` コマンドを実行してください
|
|
||||||
|
|
||||||
### コマンドツール
|
|
||||||
|
|
||||||
`go install` を使用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
#### 直接始める
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Docker Compose の使用
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Gopeed へのアクセス
|
|
||||||
|
|
||||||
docker コンテナが起動しているときは、`http://localhost:9999` からウェブページにアクセスできます。
|
|
||||||
|
|
||||||
## 寄付
|
## 寄付
|
||||||
|
|
||||||
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
|
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||||
|
|
||||||
## ショーケース
|
## ショーケース
|
||||||
|
|
||||||
@@ -75,8 +102,8 @@ docker コンテナが起動しているときは、`http://localhost:9999` か
|
|||||||
|
|
||||||
### 環境
|
### 環境
|
||||||
|
|
||||||
1. Go 言語 1.19+
|
1. Go 言語 1.23+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### クローン
|
### クローン
|
||||||
|
|
||||||
@@ -135,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" -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
|
||||||
```
|
```
|
||||||
@@ -159,7 +186,6 @@ cd ../../
|
|||||||
rm -rf cmd/web/dist
|
rm -rf cmd/web/dist
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
go run cmd/web/main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感謝
|
## 感謝
|
||||||
|
|||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/donate.html)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
|
## Giới thiệu
|
||||||
|
|
||||||
|
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
|
||||||
|
|
||||||
|
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
|
||||||
|
|
||||||
|
## Tải về
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nền tảng</th>
|
||||||
|
<th>Loại gói</th>
|
||||||
|
<th>Liên kết tải xuống</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>Bộ cài đặt EXE</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ZIP Portable</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>Bộ cài đặt DMG</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
|
||||||
|
|
||||||
|
### Command tool
|
||||||
|
|
||||||
|
Sử dụng `go install`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quyên góp
|
||||||
|
|
||||||
|
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
|
||||||
|
|
||||||
|
## Trưng bày
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`.
|
||||||
|
|
||||||
|
> Mã giao diện nằm trong thư mục `ui/flutter`.
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
1. Golang 1.23+
|
||||||
|
2. Flutter 3.16+
|
||||||
|
|
||||||
|
### Clone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Đóng góp
|
||||||
|
|
||||||
|
Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md)
|
||||||
|
|
||||||
|
### Xây dựng
|
||||||
|
|
||||||
|
#### Desktop
|
||||||
|
|
||||||
|
Trước tiên, bạn cần cấu hình môi trường theo tài liệu chính thức của [Tài liệu trang web máy tính để bàn Flutter](https://docs.flutter.dev/development/platform-integration/desktop), sau đó bạn cần đảm bảo môi trường cgo được thiết lập đúng. Để biết hướng dẫn chi tiết về cách thiết lập môi trường cgo, vui lòng tham khảo các tài liệu tương ứng có sẵn trực tuyến.
|
||||||
|
|
||||||
|
command:
|
||||||
|
|
||||||
|
- windows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build windows
|
||||||
|
```
|
||||||
|
|
||||||
|
- macos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build macos
|
||||||
|
```
|
||||||
|
|
||||||
|
- linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build linux
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mobile
|
||||||
|
|
||||||
|
Giống như trước đây, bạn cũng cần chuẩn bị môi trường `cgo` và sau đó cài đặt `gomobile`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
```
|
||||||
|
|
||||||
|
command:
|
||||||
|
|
||||||
|
- android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build apk
|
||||||
|
```
|
||||||
|
|
||||||
|
- ios
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ios --no-codesign
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web
|
||||||
|
|
||||||
|
command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tín dụng
|
||||||
|
|
||||||
|
### Người đóng góp
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
|
## Giấy phép
|
||||||
|
|
||||||
|
[GPLv3](LICENSE)
|
||||||
+74
-36
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,33 +7,84 @@
|
|||||||
[](https://docs.gopeed.com/zh/donate.html)
|
[](https://docs.gopeed.com/zh/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## 介绍
|
## 介绍
|
||||||
|
|
||||||
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
|
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
|
||||||
|
|
||||||
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
|
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
|
||||||
|
|
||||||
## 安装
|
## 下载
|
||||||
|
|
||||||
**已支持平台**
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>运行平台</th>
|
||||||
|
<th>安装包类型</th>
|
||||||
|
<th>下载链接</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
- [x] windows
|
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||||
- [x] macos
|
|
||||||
- [x] linux
|
|
||||||
- [x] android
|
|
||||||
- [x] ios
|
|
||||||
- [x] web
|
|
||||||
- [x] docker
|
|
||||||
|
|
||||||
[前往下载](https://github.com/GopeedLab/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
|
|
||||||
|
|
||||||
### 命令行工具
|
### 命令行工具
|
||||||
|
|
||||||
@@ -41,23 +94,9 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
## 浏览器扩展
|
||||||
|
|
||||||
#### 直接运行
|
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 使用 Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 访问服务
|
|
||||||
|
|
||||||
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
|
||||||
|
|
||||||
## 赞助
|
## 赞助
|
||||||
|
|
||||||
@@ -75,8 +114,8 @@ docker-compose up -d
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆项目
|
### 克隆项目
|
||||||
|
|
||||||
@@ -135,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" -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
|
||||||
```
|
```
|
||||||
@@ -159,7 +198,6 @@ cd ../../
|
|||||||
rm -rf cmd/web/dist
|
rm -rf cmd/web/dist
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
go run cmd/web/main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感谢
|
## 感谢
|
||||||
|
|||||||
+71
-37
@@ -1,3 +1,5 @@
|
|||||||
|
# [](https://gopeed.com)
|
||||||
|
|
||||||
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/GopeedLab/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/GopeedLab/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
@@ -5,11 +7,11 @@
|
|||||||
[](https://docs.gopeed.com/donate.html)
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
[](https://ko-fi.com/R6R6IJGN6)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
|
||||||
|
|
||||||
## 簡介
|
## 簡介
|
||||||
|
|
||||||
@@ -17,21 +19,72 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
|
|
||||||
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
|
||||||
|
|
||||||
## 安裝
|
## 下載
|
||||||
|
|
||||||
**已支援的平台**
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>運行平台</th>
|
||||||
|
<th>安裝包類型</th>
|
||||||
|
<th>下載鏈接</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=2>Windows</td>
|
||||||
|
<td><code>EXE Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable ZIP</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MacOS</td>
|
||||||
|
<td><code>DMG Installer</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<td><code>Flathub</code></td>
|
||||||
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>SNAP</code></td>
|
||||||
|
<td><a href="https://snapcraft.io/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>DEB</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Android</td>
|
||||||
|
<td><code>APK</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>iOS</td>
|
||||||
|
<td><code>IPA</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Web</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Docker</td>
|
||||||
|
<td></td>
|
||||||
|
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
- [x] Windows
|
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||||
- [x] MacOS
|
|
||||||
- [x] Linux
|
|
||||||
- [x] Android
|
|
||||||
- [x] iOS
|
|
||||||
- [x] Web
|
|
||||||
- [x] Docker
|
|
||||||
|
|
||||||
[下載](https://github.com/GopeedLab/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> 註:MacOS 版運行時若提示損壞,請在終端中執行 `xattr -d com.apple.quarantine /Applications/Gopeed.app`
|
|
||||||
|
|
||||||
### 使用CLI安裝
|
### 使用CLI安裝
|
||||||
|
|
||||||
@@ -41,24 +94,6 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
#### 直接執行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 使用 Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 訪問服務
|
|
||||||
|
|
||||||
當 docker 容器運作時,可以通過 `http://localhost:9999` 訪問 web 頁面。
|
|
||||||
|
|
||||||
## 贊助
|
## 贊助
|
||||||
|
|
||||||
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||||
@@ -75,8 +110,8 @@ docker-compose up -d
|
|||||||
|
|
||||||
### 開發環境
|
### 開發環境
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.10+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆項目
|
### 克隆項目
|
||||||
|
|
||||||
@@ -135,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" -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
|
||||||
```
|
```
|
||||||
@@ -159,7 +194,6 @@ cd ../../
|
|||||||
rm -rf cmd/web/dist
|
rm -rf cmd/web/dist
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
go run cmd/web/main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 感謝
|
## 感謝
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 309 KiB |
@@ -65,6 +65,9 @@ var (
|
|||||||
|
|
||||||
func printProgress(task *download.Task, title string) {
|
func printProgress(task *download.Task, title string) {
|
||||||
var rate float64
|
var rate float64
|
||||||
|
if task.Meta.Res == nil {
|
||||||
|
task = emptyTask
|
||||||
|
}
|
||||||
if task.Meta.Res.Size <= 0 {
|
if task.Meta.Res.Size <= 0 {
|
||||||
rate = 0
|
rate = 0
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+38
-6
@@ -3,11 +3,14 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest"
|
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||||
"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
|
||||||
@@ -24,17 +27,46 @@ func Start(cfg *model.StartConfig) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if downloadCfg.FirstLoad {
|
if downloadCfg.FirstLoad {
|
||||||
// Set default download dir to user download dir
|
// Set default download config
|
||||||
userDir, err := os.UserHomeDir()
|
if cfg.DownloadConfig != nil {
|
||||||
if err == nil {
|
cfg.DownloadConfig.Merge(downloadCfg)
|
||||||
downloadDir := filepath.Join(userDir, "Downloads")
|
// TODO Use PatchConfig
|
||||||
downloadCfg.DownloadDir = downloadDir
|
rest.Downloader.PutConfig(cfg.DownloadConfig)
|
||||||
rest.Downloader.PutConfig(downloadCfg)
|
downloadCfg = cfg.DownloadConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadDir := downloadCfg.DownloadDir
|
||||||
|
// Set default download dir, in docker, it will be ${exe}/Downloads, else it will be ${user}/Downloads
|
||||||
|
if downloadDir == "" {
|
||||||
|
if base.InDocker == "true" {
|
||||||
|
downloadDir = filepath.Join(filepath.Dir(cfg.StorageDir), "Downloads")
|
||||||
|
} else {
|
||||||
|
userDir, err := os.UserHomeDir()
|
||||||
|
if err == nil {
|
||||||
|
downloadDir = filepath.Join(userDir, "Downloads")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if downloadDir != "" {
|
||||||
|
downloadCfg.DownloadDir = downloadDir
|
||||||
|
rest.Downloader.PutConfig(downloadCfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}()
|
||||||
|
}
|
||||||
+16
-7
@@ -4,16 +4,20 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
Address *string `json:"address"`
|
Address *string `json:"address"`
|
||||||
Port *int `json:"port"`
|
Port *int `json:"port"`
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
ApiToken *string `json:"apiToken"`
|
ApiToken *string `json:"apiToken"`
|
||||||
|
StorageDir *string `json:"storageDir"`
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
@@ -24,7 +28,8 @@ 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.configPath = flag.String("c", "./config.json", "Config file path")
|
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -45,6 +50,9 @@ func parse() *args {
|
|||||||
if cfgArgs.ApiToken == nil {
|
if cfgArgs.ApiToken == nil {
|
||||||
cfgArgs.ApiToken = cliArgs.ApiToken
|
cfgArgs.ApiToken = cliArgs.ApiToken
|
||||||
}
|
}
|
||||||
|
if cfgArgs.StorageDir == nil {
|
||||||
|
cfgArgs.StorageDir = cliArgs.StorageDir
|
||||||
|
}
|
||||||
return cfgArgs
|
return cfgArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,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)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/cmd"
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
@@ -29,11 +31,24 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dir string
|
||||||
|
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||||
|
dir = *args.StorageDir
|
||||||
|
} else {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dir = filepath.Dir(exe)
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &model.StartConfig{
|
cfg := &model.StartConfig{
|
||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
|
StorageDir: filepath.Join(dir, "storage"),
|
||||||
ApiToken: *args.ApiToken,
|
ApiToken: *args.ApiToken,
|
||||||
|
DownloadConfig: args.DownloadConfig,
|
||||||
ProductionMode: true,
|
ProductionMode: true,
|
||||||
WebEnable: true,
|
WebEnable: true,
|
||||||
WebFS: sub,
|
WebFS: sub,
|
||||||
|
|||||||
+14
-10
@@ -1,11 +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:
|
||||||
volumes:
|
- PUID=0
|
||||||
- ~/Downloads:/root/Downloads # replace with your prefer path
|
- PGID=0
|
||||||
restart: unless-stopped
|
- UMASK=022
|
||||||
|
volumes:
|
||||||
|
- ~/gopeed/Downloads:/app/Downloads # mount download path
|
||||||
|
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
||||||
|
restart: unless-stopped
|
||||||
|
image: liwei2633/gopeed
|
||||||
|
# command: -u Username -p Password # optional authentication
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
chown -R ${PUID}:${PGID} /app
|
||||||
|
|
||||||
|
umask ${UMASK}
|
||||||
|
|
||||||
|
exec su-exec ${PUID}:${PGID} ./gopeed "$@"
|
||||||
@@ -1,108 +1,127 @@
|
|||||||
module github.com/GopeedLab/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.19
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
|
github.com/anacrolix/missinggo/v2 v2.8.0
|
||||||
github.com/anacrolix/torrent v1.52.5
|
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/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
|
||||||
go.etcd.io/bbolt v1.3.8
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/sync v0.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
|
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.2.3 // indirect
|
github.com/RoaringBitmap/roaring v1.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.3.0 // indirect
|
github.com/anacrolix/chansync v0.6.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
|
github.com/anacrolix/dht/v2 v2.22.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.2.1 // indirect
|
github.com/anacrolix/envpprof v1.4.0 // indirect
|
||||||
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // 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.0 // 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.0 // indirect
|
github.com/anacrolix/multiless v0.4.0 // indirect
|
||||||
github.com/anacrolix/stm v0.4.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.4.0 // indirect
|
github.com/anacrolix/sync v0.5.3 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||||
github.com/anacrolix/utp v0.1.0 // indirect
|
github.com/anacrolix/utp v0.2.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/immutable v0.3.0 // indirect
|
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.2.2 // indirect
|
github.com/bits-and-blooms/bitset v1.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.0 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
github.com/edsrzf/mmap-go v1.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-logr/logr v1.2.3 // indirect
|
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||||
|
github.com/go-llsqlite/crawshaw v0.5.5 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-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.3.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // 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/lispad/go-generics-tools v1.1.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.19 // 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.2 // indirect
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.4 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/pion/ice/v2 v2.2.6 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pion/interceptor v0.1.11 // 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.5 // 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.9 // indirect
|
github.com/pion/rtcp v1.2.14 // indirect
|
||||||
github.com/pion/rtp v1.7.13 // indirect
|
github.com/pion/rtp v1.8.9 // indirect
|
||||||
github.com/pion/sctp v1.8.2 // indirect
|
github.com/pion/sctp v1.8.33 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.5 // indirect
|
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.9 // indirect
|
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||||
github.com/pion/stun v0.3.5 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport v0.13.1 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/transport/v2 v2.0.0 // indirect
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
github.com/pion/webrtc/v4 v4.0.2 // indirect
|
||||||
github.com/pion/udp v0.1.4 // indirect
|
|
||||||
github.com/pion/webrtc/v3 v3.1.42 // indirect
|
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||||
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
github.com/tidwall/btree v1.6.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.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.8.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.8.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/crypto v0.29.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.14.0 // indirect
|
golang.org/x/net v0.31.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/sys v0.27.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/time v0.8.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
|
||||||
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
|
modernc.org/libc v1.61.0 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
modernc.org/sqlite v1.33.1 // indirect
|
||||||
|
zombiezen.com/go/sqlite v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
GetConfig func(v any) bool
|
GetConfig func(v any)
|
||||||
ProxyUrl *url.URL
|
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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,6 +19,8 @@ type Fetcher interface {
|
|||||||
Pause() error
|
Pause() error
|
||||||
Close() error
|
Close() error
|
||||||
|
|
||||||
|
// Stats refreshes health statistics and returns the latest information
|
||||||
|
Stats() any
|
||||||
// Meta returns the meta information of the download.
|
// Meta returns the meta information of the download.
|
||||||
Meta() *FetcherMeta
|
Meta() *FetcherMeta
|
||||||
// Progress returns the progress of the download.
|
// Progress returns the progress of the download.
|
||||||
@@ -29,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"`
|
||||||
@@ -66,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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+315
-80
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,7 @@ 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"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -35,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{
|
||||||
@@ -60,61 +59,162 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
|
|||||||
proxyListener := test.StartSocks5Server(usr, pwd)
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
|
doResolve(t, buildConfigFetcher(&base.DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
System: false,
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
Usr: usr,
|
||||||
|
Pwd: pwd,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
t.Run("Resolve Single File", func(t *testing.T) {
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
err := fetcher.Resolve(&base.Request{
|
||||||
Extra: bt.ReqExtra{
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
Trackers: []string{
|
Extra: bt.ReqExtra{
|
||||||
"udp://tracker.birkenwald.de:6969/announce",
|
Trackers: []string{
|
||||||
"udp://tracker.bitsearch.to:1337/announce",
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
if err != nil {
|
||||||
if err != nil {
|
panic(err)
|
||||||
panic(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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{
|
{
|
||||||
{
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Size: 1466714112,
|
||||||
Size: 1466714112,
|
},
|
||||||
},
|
},
|
||||||
},
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("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() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
u string
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
tests := []struct {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
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(proxyUrl *url.URL) 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
|
}
|
||||||
}
|
newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
return true
|
return proxyConfig.ToHandler()
|
||||||
}
|
}
|
||||||
newController.ProxyUrl = proxyUrl
|
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
UserAgent string `json:"userAgent"`
|
UserAgent string `json:"userAgent"`
|
||||||
Connections int `json:"connections"`
|
Connections int `json:"connections"`
|
||||||
|
UseServerCtime bool `json:"useServerCtime"`
|
||||||
}
|
}
|
||||||
@@ -3,24 +3,32 @@ 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"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"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
|
||||||
@@ -62,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,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
|
||||||
@@ -104,10 +103,11 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
Range: false,
|
Range: false,
|
||||||
Files: []*base.FileInfo{},
|
Files: []*base.FileInfo{},
|
||||||
}
|
}
|
||||||
if base.HttpCodePartialContent == httpResp.StatusCode {
|
|
||||||
// 返回206响应码表示支持断点下载
|
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
|
||||||
|
// 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)
|
||||||
@@ -117,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)
|
||||||
@@ -129,8 +130,17 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
} else {
|
} else {
|
||||||
return NewRequestError(httpResp.StatusCode, httpResp.Status)
|
return NewRequestError(httpResp.StatusCode, httpResp.Status)
|
||||||
}
|
}
|
||||||
|
// Parse last modified time
|
||||||
|
var lastModifiedTime *time.Time
|
||||||
|
lastModified := httpResp.Header.Get(base.HttpHeaderLastModified)
|
||||||
|
if lastModified != "" {
|
||||||
|
// ignore parse error
|
||||||
|
t, _ := time.Parse(time.RFC1123, lastModified)
|
||||||
|
lastModifiedTime = &t
|
||||||
|
}
|
||||||
file := &base.FileInfo{
|
file := &base.FileInfo{
|
||||||
Size: res.Size,
|
Size: res.Size,
|
||||||
|
Ctime: lastModifiedTime,
|
||||||
}
|
}
|
||||||
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
||||||
if contentDisposition != "" {
|
if contentDisposition != "" {
|
||||||
@@ -140,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)
|
||||||
}
|
}
|
||||||
@@ -149,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
|
||||||
}
|
}
|
||||||
@@ -157,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
|
||||||
}
|
}
|
||||||
@@ -167,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
|
||||||
}
|
}
|
||||||
@@ -189,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()
|
||||||
}
|
}
|
||||||
@@ -217,6 +233,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
return f.meta
|
return f.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Stats() any {
|
||||||
|
// todo http download health
|
||||||
|
return &fhttp.Stats{}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
p := make(fetcher.Progress, 0)
|
p := make(fetcher.Progress, 0)
|
||||||
if len(f.chunks) > 0 {
|
if len(f.chunks) > 0 {
|
||||||
@@ -233,24 +254,47 @@ 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
|
||||||
|
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
||||||
|
setft.SetFileTime(f.file.Name(), time.Now(), *f.meta.Res.Files[0].Ctime, *f.meta.Res.Files[0].Ctime)
|
||||||
|
}
|
||||||
f.doneCh <- err
|
f.doneCh <- err
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -259,42 +303,30 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
chunk := f.chunks[index]
|
chunk := f.chunks[index]
|
||||||
chunk.retryTimes = 0
|
chunk.retryTimes = 0
|
||||||
|
|
||||||
httpReq, err := f.buildRequest(ctx, f.meta.Req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var (
|
var (
|
||||||
client = f.buildClient()
|
client = f.buildClient()
|
||||||
buf = make([]byte, 8192)
|
buf = make([]byte, 8192)
|
||||||
maxRetries = 3
|
maxRetries = 3
|
||||||
)
|
)
|
||||||
// retry until all remain chunks failed
|
// retry until all remain chunks failed
|
||||||
for true {
|
for {
|
||||||
// if chunk is completed, return
|
// if chunk is completed, return
|
||||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||||
return
|
return 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 (
|
||||||
resp *http.Response
|
httpReq *http.Request
|
||||||
|
resp *http.Response
|
||||||
)
|
)
|
||||||
|
httpReq, err = f.buildRequest(ctx, f.meta.Req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if f.meta.Res.Range {
|
if f.meta.Res.Range {
|
||||||
httpReq.Header.Set(base.HttpHeaderRange,
|
httpReq.Header.Set(base.HttpHeaderRange,
|
||||||
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
||||||
@@ -306,28 +338,14 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
// If canceled, do not retry
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// retry request after 1 second
|
|
||||||
chunk.retryTimes = chunk.retryTimes + 1
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Http request success, reset retry times
|
|
||||||
chunk.retryTimes = 0
|
|
||||||
err = func() error {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := 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 {
|
||||||
@@ -343,6 +361,16 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
if err != nil {
|
||||||
|
// If canceled, do not retry
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// retry request after 1 second
|
||||||
|
chunk.retryTimes = chunk.retryTimes + 1
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
// if chunk is completed, reset other not complete chunks retry times
|
// if chunk is completed, reset other not complete chunks retry times
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, c := range f.chunks {
|
for _, c := range f.chunks {
|
||||||
@@ -366,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 {
|
||||||
@@ -378,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 != "" {
|
||||||
@@ -386,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 {
|
||||||
@@ -431,12 +459,17 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) buildClient() *http.Client {
|
func (f *Fetcher) buildClient() *http.Client {
|
||||||
transport := &http.Transport{}
|
transport := &http.Transport{
|
||||||
|
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)
|
||||||
if f.ctl.ProxyUrl != nil {
|
|
||||||
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
|
|
||||||
}
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Jar: jar,
|
Jar: jar,
|
||||||
@@ -447,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)
|
||||||
@@ -481,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
|
||||||
|
}
|
||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
gohttp "net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -69,7 +69,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.meta.Res) {
|
if !test.AssertResourceEqual(want, fetcher.meta.Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
|
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,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()
|
||||||
@@ -151,10 +159,12 @@ func TestFetcher_DownloadWithProxy(t *testing.T) {
|
|||||||
downloadWithProxy(httpListener, proxyListener, t)
|
downloadWithProxy(httpListener, proxyListener, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_Config(t *testing.T) {
|
func TestFetcher_ConfigConnections(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
fetcher := doDownloadReady(buildConfigFetcher(), listener, 0, t)
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
}), listener, 0, t)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -170,6 +180,76 @@ func TestFetcher_Config(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
UseServerCtime: true,
|
||||||
|
}), listener, 0, t)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fetcher.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := test.FileMd5(test.BuildFile)
|
||||||
|
got := test.FileMd5(test.DownloadFile)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Download() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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)
|
||||||
}
|
}
|
||||||
@@ -284,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 {
|
||||||
@@ -315,7 +395,13 @@ 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.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
|
ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) {
|
||||||
|
return (&base.DownloaderProxyConfig{
|
||||||
|
Enable: true,
|
||||||
|
Scheme: "socks5",
|
||||||
|
Host: proxyListener.Addr().String(),
|
||||||
|
}).ToHandler()
|
||||||
|
}
|
||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -333,22 +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() fetcher.Fetcher {
|
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherManager).Build()
|
||||||
newController := controller.NewController()
|
newController := controller.NewController()
|
||||||
mockCfg := config{
|
newController.GetConfig = func(v any) {
|
||||||
Connections: 16,
|
json.Unmarshal([]byte(test.ToJson(cfg)), v)
|
||||||
}
|
|
||||||
newController.GetConfig = func(v any) bool {
|
|
||||||
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
fetcher.Setup(newController)
|
fetcher.Setup(newController)
|
||||||
return fetcher
|
return fetcher
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+46
-20
@@ -2,14 +2,17 @@ package test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"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"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -22,6 +25,11 @@ const (
|
|||||||
Dir = "./"
|
Dir = "./"
|
||||||
BuildFile = Dir + BuildName
|
BuildFile = Dir + BuildName
|
||||||
|
|
||||||
|
ExternalDownloadUrl = "https://raw.githubusercontent.com/GopeedLab/gopeed/v1.5.6/_docs/img/banner.png"
|
||||||
|
ExternalDownloadName = "banner.png"
|
||||||
|
ExternalDownloadSize = 26416
|
||||||
|
//ExternalDownloadMd5 = "c67c6e3cae79a95342485676571e8a5c"
|
||||||
|
|
||||||
DownloadName = "download.data"
|
DownloadName = "download.data"
|
||||||
DownloadRename = "download (1).data"
|
DownloadRename = "download (1).data"
|
||||||
DownloadFile = Dir + DownloadName
|
DownloadFile = Dir + DownloadName
|
||||||
@@ -29,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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -45,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)),
|
||||||
@@ -54,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)
|
||||||
@@ -81,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++
|
||||||
@@ -101,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") != "" {
|
||||||
@@ -125,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++
|
||||||
@@ -142,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() {
|
||||||
@@ -158,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, "=")
|
||||||
@@ -197,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)
|
||||||
@@ -204,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
|
||||||
@@ -212,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])
|
||||||
@@ -243,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
|
||||||
}
|
}
|
||||||
@@ -255,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)
|
||||||
@@ -265,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)
|
||||||
@@ -282,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)
|
||||||
@@ -337,3 +355,11 @@ func StartSocks5Server(usr, pwd string) net.Listener {
|
|||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
return listener
|
return listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AssertResourceEqual(want, got *base.Resource) bool {
|
||||||
|
// Ignore ctime
|
||||||
|
if got != nil && len(got.Files) > 0 {
|
||||||
|
got.Files[0].Ctime = nil
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(want, got)
|
||||||
|
}
|
||||||
@@ -16,10 +16,13 @@ const (
|
|||||||
HttpCodePartialContent = 206
|
HttpCodePartialContent = 206
|
||||||
|
|
||||||
HttpHeaderRange = "Range"
|
HttpHeaderRange = "Range"
|
||||||
|
HttpHeaderAcceptRanges = "Accept-Ranges"
|
||||||
HttpHeaderContentLength = "Content-Length"
|
HttpHeaderContentLength = "Content-Length"
|
||||||
HttpHeaderContentRange = "Content-Range"
|
HttpHeaderContentRange = "Content-Range"
|
||||||
HttpHeaderContentDisposition = "Content-Disposition"
|
HttpHeaderContentDisposition = "Content-Disposition"
|
||||||
HttpHeaderUserAgent = "Usr-Agent"
|
HttpHeaderUserAgent = "User-Agent"
|
||||||
|
HttpHeaderLastModified = "Last-Modified"
|
||||||
|
|
||||||
|
HttpHeaderBytes = "bytes"
|
||||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||||
)
|
)
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package base
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
NotFound = errors.New("not found")
|
|
||||||
BadParams = errors.New("bad params")
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
// Version is the build version, set at build time, using `go build -ldflags "-X github.com/GopeedLab/gopeed/pkg/base.Version=1.0.0"`.
|
||||||
|
var Version string
|
||||||
|
var InDocker string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if Version == "" {
|
||||||
|
Version = "dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
+164
-9
@@ -3,7 +3,12 @@ package base
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/mattn/go-ieproxy"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request download request
|
// Request download request
|
||||||
@@ -12,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 {
|
||||||
@@ -21,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
|
||||||
@@ -59,9 +99,10 @@ func (r *Resource) CalcSize(selectFiles []int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
|
Ctime *time.Time `json:"ctime"`
|
||||||
|
|
||||||
Req *Request `json:"req"`
|
Req *Request `json:"req"`
|
||||||
}
|
}
|
||||||
@@ -89,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 {
|
||||||
@@ -126,3 +162,122 @@ func ParseOptsExtra[E any](opts *Options) error {
|
|||||||
opts.Extra = &t
|
opts.Extra = &t
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||||
|
type DownloaderStoreConfig struct {
|
||||||
|
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||||
|
|
||||||
|
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
|
||||||
|
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
|
||||||
|
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
|
||||||
|
Extra map[string]any `json:"extra"`
|
||||||
|
Proxy *DownloaderProxyConfig `json:"proxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
|
||||||
|
if cfg.MaxRunning == 0 {
|
||||||
|
cfg.MaxRunning = 5
|
||||||
|
}
|
||||||
|
if cfg.ProtocolConfig == nil {
|
||||||
|
cfg.ProtocolConfig = make(map[string]any)
|
||||||
|
}
|
||||||
|
if cfg.Proxy == nil {
|
||||||
|
cfg.Proxy = &DownloaderProxyConfig{}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
// System is the flag that use system proxy
|
||||||
|
System bool `json:"system"`
|
||||||
|
Scheme string `json:"scheme"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Usr string `json:"usr"`
|
||||||
|
Pwd string `json:"pwd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, error) {
|
||||||
|
if cfg == nil || cfg.Enable == false {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.System {
|
||||||
|
safeProxyReloadConf()
|
||||||
|
return ieproxy.GetProxyFunc()
|
||||||
|
}
|
||||||
|
if cfg.Scheme == "" || cfg.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return http.ProxyURL(util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToUrl returns the proxy url, just for git clone
|
||||||
|
func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
||||||
|
if cfg == nil || cfg.Enable == false {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.System {
|
||||||
|
safeProxyReloadConf()
|
||||||
|
static := ieproxy.GetConf().Static
|
||||||
|
if static.Active && len(static.Protocols) > 0 {
|
||||||
|
// If only one protocol, use it
|
||||||
|
if len(static.Protocols) == 1 {
|
||||||
|
for _, v := range static.Protocols {
|
||||||
|
return parseUrlSafe(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check https
|
||||||
|
if v, ok := static.Protocols["https"]; ok {
|
||||||
|
return parseUrlSafe(v)
|
||||||
|
}
|
||||||
|
// Check http
|
||||||
|
if v, ok := static.Protocols["http"]; ok {
|
||||||
|
return parseUrlSafe(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.Scheme == "" || cfg.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
u, err := url.Parse(rawUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+610
-264
File diff suppressed because it is too large.
Load diff
+343
-30
@@ -4,7 +4,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"reflect"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -37,7 +39,7 @@ func TestDownloader_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, rr.Res) {
|
if !test.AssertResourceEqual(want, rr.Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,43 +82,190 @@ 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloader_CreateDirectBatch(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 < 5; 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, 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()
|
||||||
|
|
||||||
|
tasks := downloader.GetTasks()
|
||||||
|
if 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 *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return nil
|
return nil
|
||||||
})
|
}, nil)
|
||||||
// Disable proxy
|
// Disable proxy
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Enable = false
|
proxyCfg.Enable = false
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
|
}, nil)
|
||||||
|
// Enable system proxy but not set proxy environment variable
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
|
proxyCfg.System = true
|
||||||
|
return proxyCfg
|
||||||
|
}, nil)
|
||||||
|
// Enable proxy but error proxy environment variable
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
|
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
|
||||||
|
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
|
||||||
|
proxyCfg.System = true
|
||||||
|
return proxyCfg
|
||||||
|
}, func(err error) {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("doTestDownloaderCreateWithProxy() got = nil, want error")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
// Enable system proxy and set proxy environment variable
|
||||||
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
|
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
|
||||||
|
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
|
||||||
|
proxyCfg.System = true
|
||||||
|
return proxyCfg
|
||||||
|
}, nil)
|
||||||
// Invalid proxy scheme
|
// Invalid proxy scheme
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Scheme = ""
|
proxyCfg.Scheme = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, nil)
|
||||||
// Invalid proxy host
|
// Invalid proxy host
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
proxyCfg.Host = ""
|
proxyCfg.Host = ""
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, nil)
|
||||||
// Use proxy without auth
|
// Use proxy without auth
|
||||||
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, nil)
|
||||||
// Use proxy with auth
|
// Use proxy with auth
|
||||||
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
|
doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
|
||||||
return proxyCfg
|
return proxyCfg
|
||||||
})
|
}, 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 *DownloaderProxyConfig) *DownloaderProxyConfig) {
|
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)) {
|
||||||
httpListener := test.StartTestFileServer()
|
|
||||||
defer httpListener.Close()
|
|
||||||
usr, pwd := "", ""
|
usr, pwd := "", ""
|
||||||
if auth {
|
if auth {
|
||||||
usr, pwd = "admin", "123"
|
usr, pwd = "admin", "123"
|
||||||
@@ -129,33 +278,49 @@ 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(&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: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
|
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 {
|
||||||
t.Fatal(err)
|
if errHandler == nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
errHandler(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Size: test.BuildSize,
|
Size: test.ExternalDownloadSize,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
{
|
{
|
||||||
Name: test.BuildName,
|
Name: test.ExternalDownloadName,
|
||||||
Path: "",
|
Path: "",
|
||||||
Size: test.BuildSize,
|
Size: test.ExternalDownloadSize,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, rr.Res) {
|
if !test.AssertResourceEqual(want, rr.Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,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)
|
||||||
}
|
}
|
||||||
@@ -259,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)
|
||||||
@@ -282,11 +447,11 @@ 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 := &DownloaderStoreConfig{
|
storeCfg := &base.DownloaderStoreConfig{
|
||||||
DownloadDir: "./downloads",
|
DownloadDir: "./downloads",
|
||||||
ProtocolConfig: map[string]any{
|
ProtocolConfig: map[string]any{
|
||||||
"http": map[string]any{
|
"http": map[string]any{
|
||||||
@@ -320,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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package engine
|
|||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
|
"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"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
|
||||||
@@ -10,7 +12,6 @@ import (
|
|||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/dop251/goja_nodejs/eventloop"
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
gojaurl "github.com/dop251/goja_nodejs/url"
|
gojaurl "github.com/dop251/goja_nodejs/url"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,11 +106,11 @@ 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 {
|
||||||
ProxyURL *url.URL
|
ProxyConfig *base.DownloaderProxyConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEngine(cfg *Config) *Engine {
|
func NewEngine(cfg *Config) *Engine {
|
||||||
@@ -125,13 +126,16 @@ func NewEngine(cfg *Config) *Engine {
|
|||||||
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
||||||
vm.Enable(runtime)
|
vm.Enable(runtime)
|
||||||
gojaurl.Enable(runtime)
|
gojaurl.Enable(runtime)
|
||||||
|
if err := gojaerror.Enable(runtime); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := file.Enable(runtime); err != nil {
|
if err := file.Enable(runtime); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := formdata.Enable(runtime); err != nil {
|
if err := formdata.Enable(runtime); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
|
if err := xhr.Enable(runtime, cfg.ProxyConfig.ToHandler()); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := runtime.RunString(polyfillScript); err != nil {
|
if _, err := runtime.RunString(polyfillScript); err != nil {
|
||||||
@@ -141,6 +145,10 @@ func NewEngine(cfg *Config) *Engine {
|
|||||||
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// polyfill window
|
||||||
|
if err := runtime.Set("window", runtime.GlobalObject()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
// polyfill window.location
|
// polyfill window.location
|
||||||
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
||||||
return
|
return
|
||||||
@@ -167,7 +175,19 @@ func resolveResult(value goja.Value) (any, error) {
|
|||||||
case goja.PromiseStateFulfilled:
|
case goja.PromiseStateFulfilled:
|
||||||
return p.Result().Export(), nil
|
return p.Result().Export(), nil
|
||||||
case goja.PromiseStateRejected:
|
case goja.PromiseStateRejected:
|
||||||
return nil, errors.New(p.Result().String())
|
if err, ok := p.Result().Export().(error); ok {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return export, nil
|
return export, nil
|
||||||
|
|||||||
Loaded 50 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user