Compare commits

..
167 Commits
Author SHA1 Message Date
Levi 724276ac69 release: v1.6.5 2024-12-25 22:56:09 +08:00
Levi a3f559c8e5 feat: improve file select tree view (#853) 2024-12-25 22:55:33 +08:00
Levi edd189a8a8 fix: failed to delete a running task (#852) 2024-12-24 22:23:20 +08:00
Levi c970378fd7 fix: http download retries too many times (#849) 2024-12-24 20:28:04 +08:00
Levi 8b61343aef fix: extension task missing fields (#850) 2024-12-23 01:58:29 +08:00
Levi 2cb93a03e2 perf: extension onStart hook will block task create (#844) 2024-12-21 09:58:33 +08:00
Levi 81dfc073a6 release: v1.6.4 2024-12-09 20:30:33 +08:00
Levi 427921ccab perf: write to disk only when progress updated (#839) 2024-12-09 20:29:50 +08:00
Levi f4876c8a0e fix: HTTP fetch crash when modified request extra via extension (#832) 2024-12-08 12:07:08 +08:00
Navy.Wang 35a3625b11 feat: add QNAP platform build (#828) 2024-11-29 13:38:17 +08:00
ilkei dc9b501e35 feat: optimize docker build (#827) 2024-11-27 13:40:28 +08:00
Levi 85fe92a378 release: v1.6.3 2024-11-18 18:54:46 +08:00
Levi 983d9ad09e fix: windows location fix patch not effect (#819) 2024-11-18 18:54:15 +08:00
Levi d9f757aaf0 fix: revert flutter version to v3.16.8 (#821) 2024-11-18 18:31:25 +08:00
Levi cd409fcfb9 ci: add build tests for all platforms (#812) 2024-11-15 12:06:51 +08:00
Levi 366244fa5d fix: android build on go 1.23 (#813) 2024-11-15 11:43:55 +08:00
Levi 7a2e9b4fd1 release: v1.6.2 2024-11-14 23:45:08 +08:00
Levi 203385d3f8 fix: use golang 1.23 standard library to collect crash logs (#807) 2024-11-14 00:54:34 +08:00
Levi aaf73dec24 fix: proxy config reload panic (#808) 2024-11-14 00:36:21 +08:00
Ricky 2523282a89 feat(http): add option to skip SSL certificate verification (#801) 2024-11-12 10:30:21 +08:00
Levi 3384f5197a fix: download path in docker-compose.yml (#798) 2024-11-05 09:55:25 +08:00
Levi cd77665f02 fix: avoid request locatoin permisson on windows (#787) 2024-10-19 16:55:23 +08:00
Levi a22b431150 feat: upgrade flutter to v3.24.3 (#791) 2024-10-19 12:12:41 +08:00
Levi eb7c9ad7f2 release: v1.6.1 2024-10-16 18:31:56 +08:00
Levi ea11fc38f1 ci: fix windows build version (#785) 2024-10-16 18:30:41 +08:00
towermom9 1be16e9f2c feat: Updated the indonesian translation (#777) 2024-10-10 13:18:11 +08:00
Ayase 1d86d14a20 fix: open files on linux (#765) 2024-10-03 21:40:49 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣 5ad15cc4ac feat: Update tr_tr.dart (#754) 2024-09-23 09:19:45 +08:00
Levi 49604966fd release: v1.6.0 2024-09-21 11:00:47 +08:00
Levi bdb8ed177f feat: supports batch operation of tasks by filter (#753) 2024-09-21 10:19:52 +08:00
Levi 2dc7048cba feat: support download directory white list (#752) 2024-09-20 21:32:21 +08:00
Levi d3750464aa fix: torrent stats panic (#737) 2024-09-20 12:28:24 +08:00
Levi 5c9bcd3731 fix: DotAll flag not working with group (#750) 2024-09-19 21:28:46 +08:00
Aman Raj Singh Mourya bdda550b2c feat: avoid using the depreciated background (#746) 2024-09-18 14:39:01 +08:00
liwei 3308d966f0 docs: fix banner transparent 2024-09-13 18:40:58 +08:00
Levi 18041ed9ec docs: improve banner image (#740) 2024-09-11 19:02:39 +08:00
Levi 56b85e545e release: v1.5.9 2024-09-07 19:20:12 +08:00
Levi c5af916952 feat: support default direct download config (#735) 2024-09-07 14:08:17 +08:00
Levi a1981328aa fix: avoid HTTP zero connections (#729) 2024-09-07 13:49:32 +08:00
Levi 51244133e0 feat: support extension onDone event (#728) 2024-09-07 10:10:39 +08:00
Levi 0c17093b1a feat: task separate proxy (#727) 2024-09-06 18:35:23 +08:00
Levi 6538b29b62 feat: macos round icon (#719) 2024-09-03 18:47:49 +08:00
Levi 81e90e6b89 fix: try request grant external storage on Android (#660) 2024-09-03 18:47:06 +08:00
JuanQ 4f39296664 feat: Add spanish language (#667) 2024-08-27 08:59:18 +08:00
Levi ece5b91dc2 fix: torrent redundant directory (#649) 2024-08-14 21:54:59 +08:00
Levi d9f04ae928 fix: tasks block when paused while not ready (#648) 2024-08-14 21:22:23 +08:00
Levi e3ccecbd9c fix: task directory open (#647) 2024-08-14 21:21:41 +08:00
Levi fb1c2cb060 fix: downgrade minSdkVersion (#642) 2024-08-14 21:21:09 +08:00
Levi 9bff75464d ci: docker flutter build on host (#637) 2024-08-09 18:28:44 +08:00
Levi 863f759248 release: v1.5.8 2024-08-09 12:57:11 +08:00
Levi f89fb5e438 fix: web config file not loaded when is relative path (#636) 2024-08-09 12:50:20 +08:00
Levi 7ca1cc8c64 fix: batch create tasks with duplicate names (#635) 2024-08-09 12:41:03 +08:00
Levi 1f791c66e6 perf: excessive torrent tracker dns resolve (#634) 2024-08-08 18:55:52 +08:00
Levi 4d60c47d39 feat: task detail view (#633) 2024-08-08 18:34:00 +08:00
Levi 3fd3dd49d2 feat(extension): get exception stacks on promise error (#630) 2024-08-07 12:04:45 +08:00
foxxorcat d119de7b98 feat: enhanced extension engine (#629) 2024-08-05 10:44:17 +08:00
madoka773 69b359a9c9 fix: tray icon not shown on linux flatpak or snap (#625) 2024-08-02 12:36:25 +08:00
Levi 4fbe13a1f0 fix: clear UPNP port mapping when exist app (#624) 2024-07-30 18:43:54 +08:00
Levi 3b50465708 feat: torrent seed config (#599) 2024-07-30 12:50:06 +08:00
Levi 473c58796c fix: handle empty string params (#620) 2024-07-24 11:51:31 +08:00
abc0922001 fab8f135f3 feat: Update Traditional Chinese translation (#611) 2024-07-17 18:42:42 +08:00
liwei 0fca7b6bc7 fix: fr_fr import 2024-07-15 09:36:36 +08:00
torik988 4700d0ba28 feat: Create fr_fr.dart (#608) 2024-07-15 09:28:03 +08:00
towermom9 6e31ea089f feat: Indonesian Translation/Localization (#606) 2024-07-15 09:21:49 +08:00
Levi 4a2088f6fb fix: xhr string request body can specify Content-Type (#596) 2024-07-09 18:40:02 +08:00
𝗪𝗜𝗡𝗭𝗢𝗥𝗧 17c8fb7e4d feat: update tr_tr.dart (#593) 2024-07-03 09:52:01 +08:00
Levi 8c99bbc847 ci: create CODEOWNERS 2024-06-26 17:17:27 +08:00
Levi 06c7fe7a22 fix: avoid hanging the main thread on repeat start (#584) 2024-06-20 18:59:50 +08:00
Levi 8b3b5d4394 fix: javascript engine xhr polyfill (#581) 2024-06-15 11:40:12 +08:00
liwei 2ee73ab619 fix: it_it translate 2024-06-11 14:37:39 +08:00
albanobattistella 15413dbc95 feat: Create it_it.dart (#576) 2024-06-11 14:23:24 +08:00
Levi c7c43fac80 fix: snap dir pick depend (#558) 2024-06-11 12:35:57 +08:00
247b 4283bdfa5a feat :tamil translation is added (#563) 2024-05-27 09:24:45 +08:00
liwei 328ba8592c build: update go-ieproxy depend 2024-05-23 08:57:02 +08:00
er2de2 a191a4140d feat: Update pl_pl.dart (#554) 2024-05-23 08:53:48 +08:00
Levi 3521837964 release: v1.5.7 2024-05-17 19:59:31 +08:00
Levi b30cd78f86 ci: use special flutter version on snap (#543) 2024-05-17 19:57:40 +08:00
Levi 0583e4aaa6 feat: remember delete task option (#542) 2024-05-17 18:43:21 +08:00
Levi a89c55b5c4 feat: improve Android download directory choose (#541) 2024-05-17 18:38:35 +08:00
Levi 68c40d7f3f feat: add batch create and server info APIs (#540) 2024-05-17 15:19:10 +08:00
Levi 42a0b89d95 fix: bt request extra type convert (#535) 2024-05-16 13:02:40 +08:00
liwei 1860f534cd fix: pl_pl i18n 2024-05-16 12:58:05 +08:00
er2de2 5c2d2ad7b4 feat: add Polish translation (#534) 2024-05-16 12:55:33 +08:00
Levi db6352ada8 feat: add show item on system tray (#533) 2024-05-15 20:41:57 +08:00
Levi d1a5ed8b3d feat: support launch at startup (#532) 2024-05-14 19:23:29 +08:00
Levi be9583be0f ci: fix ios framework build (#525) 2024-05-08 18:06:58 +08:00
Levi d2c70b077b feat: check API port is in use (#524) 2024-05-07 19:19:57 +08:00
Levi 53b3eea4fc feat: add extension store links for edge and firefox (#520) 2024-05-06 18:36:53 +08:00
Levi 54347f41b6 feat: support double click to open task file (#516) 2024-05-02 13:22:57 +08:00
Levi cea454b015 feat: support system proxy (#515) 2024-05-01 18:53:12 +08:00
Levi 070db64ca9 fix: http request cookie incremented indefinitely on retries (#514) 2024-05-01 17:45:59 +08:00
Levi 6a8956b04d docs: fix linux download link 2024-05-01 04:16:56 +00:00
Deepansh Singh Arya 057504bb81 feat: updated iOS icons (#512) 2024-05-01 10:48:35 +08:00
madoka773 470b488824 ci: build gopeed with libayatana-appindicator (#508) 2024-04-26 15:41:47 +08:00
madoka773 c8c9df1ae8 ci: build linux with ubuntu latest (#501) 2024-04-23 08:57:23 +08:00
invictus1032 33d787c74e docs: fix Linux download links (#465)
Fix filename discrepancies for .deb and .AppImage
2024-04-12 14:45:09 +08:00
liwei 67338a5e83 docs: add browser extension link 2024-04-03 17:52:37 +08:00
Levi 723697192d fix: command panic before resolve resource (#451) 2024-04-03 16:56:20 +08:00
Zack 4294d07d6a fix: Fix the issue of installing apk files(Android) (#448) (#449) 2024-04-03 09:23:05 +08:00
WINZORT 3936fd786f feat: Update tr_tr.dart (#445) 2024-04-01 09:07:44 +08:00
ilkeiii 45197e9a3b fix: docker entrypoint_sh (#434) 2024-03-30 12:35:00 +08:00
liwei c9692e76e5 fix: build flags type 2024-03-30 11:10:34 +08:00
liwei 0a9e39ed75 ci: fix Dockerfile InDocker flag 2024-03-30 11:00:14 +08:00
liwei 1971f582b5 ci: fix Dockerfile build 2024-03-30 10:50:53 +08:00
liwei c62d6be3da ci: fix Dockerfile build args 2024-03-30 10:39:46 +08:00
liwei 7d5e503894 fix: bump rounded_loading_button 2024-03-30 10:21:19 +08:00
liwei 6c4066fde4 ci: fix Dockerfile 2024-03-30 10:05:45 +08:00
liwei ffdf3e717f ci: fix docker build args format 2024-03-30 09:57:21 +08:00
liwei f66814b4ae ci: fix docker build args 2024-03-30 09:50:41 +08:00
Levi 0c3e201a51 release: v1.5.6 2024-03-30 09:35:20 +08:00
liwei c32a5d750f ci: update change log template 2024-03-30 09:32:43 +08:00
Levi 207c287c97 feat: add browser extension link (#433) 2024-03-30 09:12:24 +08:00
Levi 56ef25065e fix: task lock by unreachable torrent (#431) 2024-03-29 22:11:21 +08:00
Levi bd6d6be71f feat: web config add stroage dir item (#428) 2024-03-29 17:10:25 +08:00
Zack 1d3a028bae docs: Optimize compile commands (#420) 2024-03-27 22:18:52 +08:00
Zack 27011fb96b feat: Support Via Download task, require via version 5.5.0 or above(Android) (#412) (#419) 2024-03-27 21:58:16 +08:00
ilkeiii c696b59c4b feat: add docker permission related settings (#380) 2024-03-25 08:51:07 +08:00
WINZORT 78b5b8a78b feat: Create tr_tr.dart (#402) 2024-03-12 13:57:11 +08:00
Levi c81ca7f5cd feat: docker download dir move to /app/Download (#398) 2024-03-12 13:39:56 +08:00
liwei e39d50fe09 docs: add Flathub link 2024-02-23 08:53:20 +08:00
David Nguyen 086269f8bd feat: add Vietnamese translate (#389) 2024-02-22 15:54:35 +08:00
liwei 00d2183961 docs: fix download link 2024-02-18 20:45:07 +08:00
liwei 5f914dc4c4 docs: fix mac download link 2024-02-18 12:47:27 +08:00
MonoYan 9bf204cf04 Update README_zh-CN.md (#381)
Fixed MacOS download link
2024-02-18 01:14:56 +08:00
Levi a816b9ccc8 fix: create task with default select files (#374) 2024-02-02 09:44:00 +08:00
liwei d60d370463 docs: remove linux portable link 2024-02-01 15:11:41 +08:00
Levi 84d054b494 fix: start duplicate server on mobile when the app is closed by swiping (#370) 2024-01-31 18:45:09 +08:00
Elliot 1aa96e147f feat: improve zh-TW translation (#369) 2024-01-31 18:23:25 +08:00
Levi 8c01e45780 release: v1.5.5 2024-01-30 14:50:29 +08:00
Levi 8ad571aa08 fix: config init fail (#365) 2024-01-30 14:49:26 +08:00
Levi 39316e9b8e fix: windows default install dir (#366) 2024-01-30 14:41:22 +08:00
liwei f778596390 release: v1.5.4 2024-01-30 10:57:08 +08:00
liwei c2cbc14492 docs: improve download guide 2024-01-30 10:37:27 +08:00
Levi 62c22bc6b4 feat: support use server time for file creation (#359) 2024-01-30 09:08:01 +08:00
Levi b89c7bc608 feat: migrate shared_preferences to hive (#343) 2024-01-24 15:49:51 +08:00
Tracey Lesley f2cd686563 fix: missing pb size format (#342) 2024-01-24 14:33:18 +08:00
Levi ef5ee99f04 feat: custom torrent client version (#341) 2024-01-23 17:44:44 +08:00
Tracey Lesley 6d136604db feat: downloader task stats (#339) 2024-01-22 11:43:12 +08:00
Levi 38ff711ea9 feat: add batch delete tasks api (#338) 2024-01-19 18:04:54 +08:00
liwei a1a8abc25f fix: ios download path 2024-01-19 15:45:24 +08:00
madoka773 abccbabc78 fix: wrong debian installed size (#335) 2024-01-19 11:17:50 +08:00
Levi c26eb26b8f fix: default download path on mobile (#337) 2024-01-19 11:12:27 +08:00
Levi 74beea6044 feat: auto create torrent task for http download (#334) 2024-01-18 18:38:44 +08:00
Levi e548d7e0c9 feat: save window size and resize on startup (#332) 2024-01-18 15:00:05 +08:00
madoka773 a996d57914 ci: fix debian 12 depends (#329) 2024-01-18 11:16:50 +08:00
Levi f7dc60ad50 fix: proxy setting only numeric input (#328) 2024-01-18 09:17:06 +08:00
Levi 8425a96ac9 feat: add mirror.ghproxy.com trackers subscribe cdn (#330) 2024-01-17 22:09:37 +08:00
Tracey Lesley 99f4e8c895 feat: add download stats info (#323) 2024-01-17 21:59:01 +08:00
Elliot cc0f61b527 feat: improve zh-TW translation (#327) 2024-01-17 18:16:40 +08:00
Levi 922ef969a6 feat: add thanks item for contributors (#325) 2024-01-16 19:02:20 +08:00
Tracey Lesley 3a542054fc fix: compatible with not follow the http range response standard 2024-01-14 09:58:14 +08:00
Levi 895a8d2ee4 fix: foreground task scope (#314) 2024-01-05 21:49:21 +08:00
madoka773 29d0f5ebcf ci: use flutter distributor (#313) 2024-01-04 12:46:16 +08:00
Levi 31228e3edf release: v1.5.3 2023-12-15 19:23:16 +08:00
Levi 31a32d7e14 fix: handle api panic (#302) 2023-12-15 19:20:56 +08:00
Levi 65fd6fb489 fix: http pause handle (#303) 2023-12-15 19:15:38 +08:00
Levi 953b878a89 fix: adapt mobile proxy settings ui (#301) 2023-12-15 19:09:48 +08:00
Elliot ae4ca3da58 feat: improve zh-TW translation (#300) 2023-12-14 18:06:49 +08:00
Levi d09bd180af release: v1.5.2 2023-12-13 09:38:34 +08:00
liwei 3d24e353c5 docs: docker data path mounted 2023-12-13 08:57:57 +08:00
Levi ce7489b0c0 feat: support volume storage dir on docker (#291) 2023-12-13 08:51:22 +08:00
Levi 2b5091ba21 feat: improve extension engine (#298)
1. support onError event
2. add built-in MessageError
2023-12-12 19:00:42 +08:00
Levi ff32971aca fix: repair invalid filename (#296) 2023-12-12 18:40:36 +08:00
Levi f2af61f9aa fix: sort of completed tasks (#297) 2023-12-12 18:36:40 +08:00
Elliot d935b9d631 feat: improve zh_TW translate (#287) 2023-12-04 23:01:49 +08:00
liwei ae66d1d83d docs: fix contributing 2023-12-04 13:41:27 +08:00
madoka773 aa91ca1d50 ci: linux build use libappindicator (#286) 2023-12-04 13:36:27 +08:00
200 changed files with 10089 additions and 5613 deletions

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
.git
*.data
*.log
node_modules
**/node_modules
Dockerfile
.dockerignore
.github
_docs
_examples
bin
ui
+1
View File
@@ -0,0 +1 @@
* @monkeyWie
+7 -2
View File
@@ -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
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile" - "Dockerfile"
env: env:
GO_VERSION: "1.19" GO_VERSION: "1.23"
permissions: permissions:
contents: write contents: write
+99 -12
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
+29
View File
@@ -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:
+1 -1
View File
@@ -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开发
+1 -1
View File
@@ -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
View File
@@ -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"]
+74 -37
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,34 +7,84 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[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
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,11 +7,11 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[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
View File
@@ -0,0 +1,213 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Trạng thái kiểm tra](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Phiên bản](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Tải về](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Ủng hộ](https://img.shields.io/badge/%24-ủng%20hộ-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
<a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[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
![](_docs/img/ui-demo.png)
## 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
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## Giấy phép
[GPLv3](LICENSE)
+74 -36
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,33 +7,84 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/zh/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[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
View File
@@ -1,3 +1,5 @@
# [![](_docs/img/banner.png)](https://gopeed.com)
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
@@ -5,11 +7,11 @@
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html) [![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) <a href="https://trendshift.io/repositories/7953" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7953" alt="GopeedLab%2Fgopeed | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[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

+3
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+15
View File
@@ -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
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
chown -R ${PUID}:${PGID} /app
umask ${UMASK}
exec su-exec ${PUID}:${PGID} ./gopeed "$@"
+83 -64
View File
@@ -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
) )
+275 -183
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -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
},
} }
} }
+54 -8
View File
@@ -4,14 +4,12 @@ import (
"github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"path" "path"
"strings"
) )
// Fetcher defines the interface for a download protocol. // Fetcher defines the interface for a download protocol.
// One fetcher for each download task // Each download task will have a corresponding Fetcher instance for the management of the download task
type Fetcher interface { type Fetcher interface {
// Name return the name of the protocol.
Name() string
Setup(ctl *controller.Controller) Setup(ctl *controller.Controller)
// Resolve resource info from request // Resolve resource info from request
Resolve(req *base.Request) error Resolve(req *base.Request) error
@@ -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 {
+97
View File
@@ -0,0 +1,97 @@
package fetcher
import "testing"
func TestSchemeFilter_Match(t *testing.T) {
type fields struct {
Type FilterType
Pattern string
}
type args struct {
uri string
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "url match",
fields: fields{
Type: FilterTypeUrl,
Pattern: "https",
},
args: args{
uri: "https://github.com",
},
want: true,
},
{
name: "url not match",
fields: fields{
Type: FilterTypeUrl,
Pattern: "https",
},
args: args{
uri: "ftp://github.com",
},
want: false,
},
{
name: "file match",
fields: fields{
Type: FilterTypeFile,
Pattern: "torrent",
},
args: args{
uri: "d:/temp/test.torrent",
},
want: true,
},
{
name: "file not match",
fields: fields{
Type: FilterTypeFile,
Pattern: "torrent",
},
args: args{
uri: "d:/temp/test.txt",
},
want: false,
},
{
name: "base64 match",
fields: fields{
Type: FilterTypeBase64,
Pattern: "application/x-bittorrent",
},
args: args{
uri: "data:application/x-bittorrent;base64,xxx",
},
want: true,
},
{
name: "base64 not match",
fields: fields{
Type: FilterTypeBase64,
Pattern: "application/x-bittorrent",
},
args: args{
uri: "data:application/javascript;base64,xxx",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SchemeFilter{
Type: tt.fields.Type,
Pattern: tt.fields.Pattern,
}
if got := s.Match(tt.args.uri); got != tt.want {
t.Errorf("Match() = %v, want %v", got, tt.want)
}
})
}
}
+6
View File
@@ -3,4 +3,10 @@ package bt
type config struct { type config struct {
ListenPort int `json:"listenPort"` ListenPort int `json:"listenPort"`
Trackers []string `json:"trackers"` Trackers []string `json:"trackers"`
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
SeedKeep bool `json:"seedKeep`
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
SeedRatio float64 `json:"seedRatio"`
// SeedTime is the time in seconds to seed after downloading is complete.
SeedTime int64 `json:"seedTime"`
} }
@@ -0,0 +1,49 @@
package bt
import (
"context"
"net"
"time"
"github.com/rs/dnscache"
)
// DnsCacheResolver resolves DNS requests for an HTTP client using an in-memory cache.
type DnsCacheResolver struct {
RefreshTimeout time.Duration
resolver dnscache.Resolver
}
func (r *DnsCacheResolver) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ips, err := r.resolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
var conn net.Conn
for _, ip := range ips {
var dialer net.Dialer
conn, err = dialer.DialContext(ctx, network, net.JoinHostPort(ip, port))
if err == nil {
break
}
}
return conn, err
}
func (r *DnsCacheResolver) Run(ctx context.Context) {
ticker := time.NewTicker(r.RefreshTimeout)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.resolver.Refresh(true)
}
}
}
File diff suppressed because it is too large. Load diff
+136 -36
View File
@@ -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
} }
+2 -1
View File
@@ -3,6 +3,7 @@
package bt package bt
import ( import (
"context"
"fmt" "fmt"
"github.com/anacrolix/torrent/storage" "github.com/anacrolix/torrent/storage"
"io" "io"
@@ -53,7 +54,7 @@ func (fs fileClientImpl) Close() error {
return fs.opts.PieceCompletion.Close() return fs.opts.PieceCompletion.Close()
} }
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) { func (fs fileClientImpl) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
upvertedFiles := info.UpvertedFiles() upvertedFiles := info.UpvertedFiles()
files := make([]file, 0, len(upvertedFiles)) files := make([]file, 0, len(upvertedFiles))
for _, fileInfo := range upvertedFiles { for _, fileInfo := range upvertedFiles {
Binary file not shown.
+3 -2
View File
@@ -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"`
} }
+159 -85
View File
@@ -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
}
+104 -19
View File
@@ -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
+40
View File
@@ -0,0 +1,40 @@
package http
import (
"context"
"io"
"time"
)
type TimeoutReader struct {
reader io.Reader
timeout time.Duration
}
func NewTimeoutReader(r io.Reader, timeout time.Duration) *TimeoutReader {
return &TimeoutReader{
reader: r,
timeout: timeout,
}
}
func (tr *TimeoutReader) Read(p []byte) (n int, err error) {
ctx, cancel := context.WithTimeout(context.Background(), tr.timeout)
defer cancel()
done := make(chan struct{})
var readErr error
var bytesRead int
go func() {
bytesRead, readErr = tr.reader.Read(p)
close(done)
}()
select {
case <-done:
return bytesRead, readErr
case <-ctx.Done():
return 0, ctx.Err()
}
}
@@ -0,0 +1,51 @@
package http
import (
"bytes"
"context"
"errors"
"io"
"testing"
"time"
)
func TestTimeoutReader_Read(t *testing.T) {
data := []byte("Hello, World!")
reader := bytes.NewReader(data)
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
buf := make([]byte, len(data))
n, err := timeoutReader.Read(buf)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if n != len(data) {
t.Fatalf("expected to read %d bytes, read %d", len(data), n)
}
if !bytes.Equal(buf, data) {
t.Fatalf("expected %s, got %s", data, buf)
}
}
func TestTimeoutReader_ReadTimeout(t *testing.T) {
reader := &slowReader{delay: 2 * time.Second}
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
buf := make([]byte, 8192)
_, err := timeoutReader.Read(buf)
if err == nil {
t.Fatal("expected timeout error, got nil")
}
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("expected %v, got %v", context.DeadlineExceeded, err)
}
}
type slowReader struct {
delay time.Duration
}
func (sr *slowReader) Read(p []byte) (n int, err error) {
time.Sleep(sr.delay)
return 0, io.EOF
}
+46 -20
View File
@@ -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)
}
+4 -1
View File
@@ -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"
) )
-8
View File
@@ -1,8 +0,0 @@
package base
import "errors"
var (
NotFound = errors.New("not found")
BadParams = errors.New("bad params")
)
+11
View File
@@ -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
View File
@@ -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
}
+264
View File
@@ -0,0 +1,264 @@
package base
import (
"reflect"
"testing"
)
func TestDownloaderStoreConfig_Init(t *testing.T) {
tests := []struct {
name string
fields *DownloaderStoreConfig
want *DownloaderStoreConfig
}{
{
"Init",
&DownloaderStoreConfig{},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
},
},
{
"Init MaxRunning",
&DownloaderStoreConfig{
MaxRunning: 10,
},
&DownloaderStoreConfig{
MaxRunning: 10,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{},
},
},
{
"Init ProtocolConfig",
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "value",
},
},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{
"key": "value",
},
Proxy: &DownloaderProxyConfig{},
},
},
{
"Init Proxy",
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Enable: true,
},
},
&DownloaderStoreConfig{
MaxRunning: 5,
ProtocolConfig: map[string]any{},
Proxy: &DownloaderProxyConfig{
Enable: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &DownloaderStoreConfig{
FirstLoad: tt.fields.FirstLoad,
DownloadDir: tt.fields.DownloadDir,
MaxRunning: tt.fields.MaxRunning,
ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
}
if got := cfg.Init(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Init() = %v, want %v", got, tt.want)
}
})
}
}
func TestDownloaderStoreConfig_Merge(t *testing.T) {
type args struct {
beforeCfg *DownloaderStoreConfig
}
tests := []struct {
name string
fields *DownloaderStoreConfig
args args
want *DownloaderStoreConfig
}{
{
"Merge Nil",
&DownloaderStoreConfig{},
args{
beforeCfg: nil,
},
&DownloaderStoreConfig{},
},
{
"Merge DownloadDir No Override",
&DownloaderStoreConfig{
DownloadDir: "before",
},
args{
beforeCfg: &DownloaderStoreConfig{
DownloadDir: "after",
},
},
&DownloaderStoreConfig{
DownloadDir: "before",
},
},
{
"Merge DownloadDir Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
DownloadDir: "after",
},
},
&DownloaderStoreConfig{
DownloadDir: "after",
},
},
{
"Merge MaxRunning No Override",
&DownloaderStoreConfig{
MaxRunning: 1,
},
args{
beforeCfg: &DownloaderStoreConfig{
MaxRunning: 10,
},
},
&DownloaderStoreConfig{
MaxRunning: 1,
},
},
{
"Merge MaxRunning Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
MaxRunning: 10,
},
},
&DownloaderStoreConfig{
MaxRunning: 10,
},
},
{
"Merge ProtocolConfig No Override",
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{},
},
args{
beforeCfg: &DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{},
},
},
{
"Merge ProtocolConfig Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
ProtocolConfig: map[string]any{
"key": "after",
},
},
},
{
"Merge Extra No Override",
&DownloaderStoreConfig{
Extra: map[string]any{},
},
args{
beforeCfg: &DownloaderStoreConfig{
Extra: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
Extra: map[string]any{},
},
},
{
"Merge Extra Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
Extra: map[string]any{
"key": "after",
},
},
},
&DownloaderStoreConfig{
Extra: map[string]any{
"key": "after",
},
},
},
{
"Merge Proxy No Override",
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{},
},
args{
beforeCfg: &DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Scheme: "http",
},
},
},
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{},
},
},
{
"Merge Proxy Override",
&DownloaderStoreConfig{},
args{
beforeCfg: &DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Scheme: "http",
},
},
},
&DownloaderStoreConfig{
Proxy: &DownloaderProxyConfig{
Scheme: "http",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &DownloaderStoreConfig{
FirstLoad: tt.fields.FirstLoad,
DownloadDir: tt.fields.DownloadDir,
MaxRunning: tt.fields.MaxRunning,
ProtocolConfig: tt.fields.ProtocolConfig,
Extra: tt.fields.Extra,
Proxy: tt.fields.Proxy,
}
if got := cfg.Merge(tt.args.beforeCfg); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Merge() = %v, want %v", got, tt.want)
}
})
}
}
+610 -264
View File
File diff suppressed because it is too large. Load diff
+343 -30
View File
@@ -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)
}
})
}
+25 -5
View File
@@ -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