mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 03:43:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c01e45780 | ||
|
|
8ad571aa08 | ||
|
|
39316e9b8e | ||
|
|
f778596390 | ||
|
|
c2cbc14492 | ||
|
|
62c22bc6b4 | ||
|
|
b89c7bc608 | ||
|
|
f2cd686563 | ||
|
|
ef5ee99f04 | ||
|
|
6d136604db | ||
|
|
38ff711ea9 | ||
|
|
a1a8abc25f | ||
|
|
abccbabc78 | ||
|
|
c26eb26b8f | ||
|
|
74beea6044 | ||
|
|
e548d7e0c9 | ||
|
|
a996d57914 | ||
|
|
f7dc60ad50 | ||
|
|
8425a96ac9 | ||
|
|
99f4e8c895 | ||
|
|
cc0f61b527 | ||
|
|
922ef969a6 | ||
|
|
3a542054fc | ||
|
|
895a8d2ee4 | ||
|
|
29d0f5ebcf | ||
|
|
31228e3edf | ||
|
|
31a32d7e14 | ||
|
|
65fd6fb489 | ||
|
|
953b878a89 | ||
|
|
ae4ca3da58 | ||
|
|
d09bd180af | ||
|
|
3d24e353c5 | ||
|
|
ce7489b0c0 | ||
|
|
2b5091ba21 | ||
|
|
ff32971aca | ||
|
|
f2af61f9aa | ||
|
|
d935b9d631 | ||
|
|
ae66d1d83d | ||
|
|
aa91ca1d50 | ||
|
|
2d4bdea5cb | ||
|
|
2f2393ac20 | ||
|
|
5a761e244a | ||
|
|
6538766780 | ||
|
|
16b7a3d5b3 | ||
|
|
ef97528114 | ||
|
|
b58669bb46 | ||
|
|
51d28c5e53 | ||
|
|
db15650664 | ||
|
|
ee7da179c1 | ||
|
|
6bb32372ab | ||
|
|
7d6428c967 | ||
|
|
bba5107dd5 | ||
|
|
ddfe76b635 | ||
|
|
aad7395dde | ||
|
|
6f541715df | ||
|
|
848797b2fd | ||
|
|
0a05bf41e7 | ||
|
|
485656d1d4 | ||
|
|
c074e0c2bc | ||
|
|
0db0a2c987 | ||
|
|
b06a79b5c7 | ||
|
|
4840c4b8a4 | ||
|
|
e72b097aa2 | ||
|
|
af52c4ce7e | ||
|
|
aae79c2449 | ||
|
|
83838afce9 | ||
|
|
4690107e32 | ||
|
|
74b08b3631 | ||
|
|
953d348c0c | ||
|
|
00cea68b6e | ||
|
|
daa9d4c707 | ||
|
|
6bc587752a | ||
|
|
11befc09bd | ||
|
|
ff6c0da262 | ||
|
|
aab4a37a7e | ||
|
|
74c9491893 | ||
|
|
256faf2cbd | ||
|
|
32e3620f5e | ||
|
|
39202b44a1 | ||
|
|
457d9e85d6 | ||
|
|
858ad74339 | ||
|
|
755d7ce814 | ||
|
|
854f600d3f | ||
|
|
28da89f08c | ||
|
|
dddefb68a2 | ||
|
|
e882b663a8 | ||
|
|
6f8e9fc68f | ||
|
|
7799a4032c | ||
|
|
6316e9986a | ||
|
|
39d1028ee9 | ||
|
|
107f90649b | ||
|
|
c8b66501b7 | ||
|
|
398e3f7adc | ||
|
|
158018c248 | ||
|
|
c56447ad5f | ||
|
|
c073fc0a67 | ||
|
|
82d316af83 | ||
|
|
150953a033 | ||
|
|
f30ad6641f | ||
|
|
7019f18635 | ||
|
|
c0338c3d8e | ||
|
|
fe55f1bd54 | ||
|
|
48f8c60672 | ||
|
|
cc97cedf0f | ||
|
|
a7d855f65a | ||
|
|
f0f16b3c80 | ||
|
|
3459b474db | ||
|
|
d28fcef0a7 | ||
|
|
d694c08635 | ||
|
|
a7c61a668e | ||
|
|
09aea55b49 | ||
|
|
c0cd2d7255 | ||
|
|
b00f4a1ed0 | ||
|
|
06766eea56 | ||
|
|
0f0c8f27bd | ||
|
|
45cf62f88d | ||
|
|
6e9d3499d9 | ||
|
|
b6d60f6bdf | ||
|
|
e1684e755a | ||
|
|
e63cfca82a | ||
|
|
85ef8d3be4 | ||
|
|
bf2ccdb5a7 | ||
|
|
a75bfd6118 | ||
|
|
a5d8713ff7 | ||
|
|
81518d794f | ||
|
|
7c2b81caf7 | ||
|
|
2221332243 | ||
|
|
dc407434d0 | ||
|
|
1c2e86a991 | ||
|
|
c78dc5f18f | ||
|
|
8ea5abb5c3 | ||
|
|
d1a4067dfa | ||
|
|
a1928d202f | ||
|
|
7376e85895 | ||
|
|
eb79dd5ac5 | ||
|
|
5b945b16c1 | ||
|
|
80e60f4190 | ||
|
|
3087796ef5 | ||
|
|
99de0aaf9b | ||
|
|
6bea61cf0a | ||
|
|
6c3553805e | ||
|
|
adc7c7766d | ||
|
|
15d0296307 | ||
|
|
32d2e46118 | ||
|
|
abe3cec13d | ||
|
|
9249613601 | ||
|
|
5bafb40599 | ||
|
|
5968d3c14a | ||
|
|
fcb54d3acc | ||
|
|
e2c4567ed0 | ||
|
|
c3b28c7034 | ||
|
|
32061200a5 | ||
|
|
19aad9bc9c | ||
|
|
2d0cc1c877 | ||
|
|
dc94a1004c | ||
|
|
1385f62fb0 | ||
|
|
34fc886662 | ||
|
|
c651263747 | ||
|
|
96a4526bc8 | ||
|
|
97cf8ff0c5 | ||
|
|
70c16195ef | ||
|
|
eb9d966855 | ||
|
|
0e3ca912a7 | ||
|
|
b3e372326f | ||
|
|
0100f0131d | ||
|
|
e4c3b449a4 | ||
|
|
aaa8cd6406 | ||
|
|
328422b3b8 | ||
|
|
17cce89ef1 | ||
|
|
d9c5ca339f | ||
|
|
1e08ecd8db | ||
|
|
34bcbd9571 | ||
|
|
09bdb5584a | ||
|
|
36f5c9569e | ||
|
|
3d9edc5133 | ||
|
|
ed08c05532 | ||
|
|
9bd8befcb8 | ||
|
|
7ba87e48fc | ||
|
|
af5f739630 | ||
|
|
5558cac263 | ||
|
|
4128e718e9 | ||
|
|
6d017b18f5 | ||
|
|
35eb089178 | ||
|
|
b5f78f0fbe | ||
|
|
628d73f90d | ||
|
|
7d2d84d844 | ||
|
|
1cbe58d760 | ||
|
|
c77a4094a3 | ||
|
|
33f6325bf7 | ||
|
|
701ea59035 | ||
|
|
e77441f0de | ||
|
|
dd46e7a3eb | ||
|
|
f42838810a | ||
|
|
7fdba12376 | ||
|
|
20130ae6d0 | ||
|
|
4598e9b351 | ||
|
|
2e639e0fef | ||
|
|
dbbbc204fb | ||
|
|
caab121cc2 | ||
|
|
c637b36094 | ||
|
|
38ba901956 | ||
|
|
8bae1a5850 | ||
|
|
56375c0edb | ||
|
|
5b69472ac8 | ||
|
|
dbc9c4c121 | ||
|
|
c0e24ce869 | ||
|
|
74c24ad860 | ||
|
|
9b1ed15fea | ||
|
|
a875a9258e | ||
|
|
0d2ac10af8 | ||
|
|
978b8bc7cb | ||
|
|
50c3713c90 | ||
|
|
2639b6bc65 | ||
|
|
8b29ceb151 | ||
|
|
e1c14ad9f8 | ||
|
|
e4f86097e8 | ||
|
|
0515ce6081 | ||
|
|
048554df4d | ||
|
|
2d02486ca6 | ||
|
|
3523f5855a | ||
|
|
aa9577a2e0 | ||
|
|
efc9dcca0b | ||
|
|
b062336c09 | ||
|
|
baf88a5cb7 | ||
|
|
e208014d34 | ||
|
|
3ecedd60fd | ||
|
|
6fd68e4752 | ||
|
|
c2910b0e8c | ||
|
|
8ed51787ad | ||
|
|
2e5fd3017c | ||
|
|
027d29e834 | ||
|
|
1a3c8c026b | ||
|
|
17cb59c606 | ||
|
|
e3828c2982 | ||
|
|
8cef0bb727 | ||
|
|
21ec1e4539 | ||
|
|
dda44cc770 | ||
|
|
78911b2aa9 | ||
|
|
4935dcd0e6 | ||
|
|
e8fe6ad550 | ||
|
|
db4003ad4f | ||
|
|
0a4be165a5 | ||
|
|
c62867e3ef | ||
|
|
6df4959845 | ||
|
|
e0b18013be | ||
|
|
d205e3b372 | ||
|
|
719daf054b | ||
|
|
a5f72a22c3 | ||
|
|
bf5c7655f2 | ||
|
|
691c2f496e | ||
|
|
9ef3405e43 | ||
|
|
215faa0bf0 | ||
|
|
695da7ea87 | ||
|
|
7c6c361d30 | ||
|
|
872ba0ba87 | ||
|
|
93c14dcee5 | ||
|
|
1af0b2175c | ||
|
|
ee81ebffef | ||
|
|
dc00fd2543 |
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
#patreon: # Replace with a single Patreon username
|
||||||
|
#open_collective: # Replace with a single Open Collective username
|
||||||
|
#ko_fi: # Replace with a single Ko-fi username
|
||||||
|
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
#liberapay: # Replace with a single Liberapay username
|
||||||
|
#issuehunt: # Replace with a single IssueHunt username
|
||||||
|
#otechie: # Replace with a single Otechie username
|
||||||
|
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
# github: monkeyWie
|
||||||
|
ko_fi: gopeed
|
||||||
|
custom: https://docs.gopeed.com/donate.html
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<!--
|
<!--
|
||||||
请根据以下模版提issue
|
Please use the following template to create an issue.
|
||||||
-->
|
-->
|
||||||
### 问题描述(必要)
|
### Description(required)
|
||||||
### 版本号(必要)
|
### App Version(required)
|
||||||
### 操作系统(必要)
|
### OS Version(required)
|
||||||
### 相关截图
|
### Snapshots
|
||||||
### 相关日志
|
### Log
|
||||||
@@ -11,12 +11,16 @@ categories:
|
|||||||
- 'fix'
|
- 'fix'
|
||||||
- 'bugfix'
|
- 'bugfix'
|
||||||
- 'bug'
|
- 'bug'
|
||||||
|
- title: '🧪 Tests'
|
||||||
|
label: 'test'
|
||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
label: 'chore'
|
label: 'chore'
|
||||||
- title: '📖 Document'
|
- title: '📖 Document'
|
||||||
label: 'doc'
|
label: 'doc'
|
||||||
- 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:
|
||||||
@@ -30,6 +34,10 @@ version-resolver:
|
|||||||
- 'patch'
|
- 'patch'
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
## Changes
|
# Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
# 更新日志
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
+269
-122
File diff suppressed because it is too large.
Load diff
@@ -11,8 +11,16 @@ on:
|
|||||||
- "pkg/**"
|
- "pkg/**"
|
||||||
- "ui/**"
|
- "ui/**"
|
||||||
- ".github/workflows/release.yml"
|
- ".github/workflows/release.yml"
|
||||||
|
- ".github/release-drafter.yml"
|
||||||
- "go.mod"
|
- "go.mod"
|
||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
- "Dockerfile"
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: "1.19"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -23,9 +31,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.19"
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v5
|
||||||
id: create_release
|
id: create_release
|
||||||
env:
|
env:
|
||||||
|
|||||||
+20
-10
@@ -26,6 +26,10 @@ on:
|
|||||||
- "go.mod"
|
- "go.mod"
|
||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: "1.21"
|
||||||
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
# name: Lint
|
# name: Lint
|
||||||
@@ -41,30 +45,36 @@ jobs:
|
|||||||
# go get -u golang.org/x/lint/golint
|
# go get -u golang.org/x/lint/golint
|
||||||
# golint -set_exit_status ./...
|
# golint -set_exit_status ./...
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.19"
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache: true
|
|
||||||
- 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@v3
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
fail_ci_if_error: true
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# needs: [lint, test]
|
# needs: [lint, test]
|
||||||
needs: [test]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.19"
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache: true
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build github.com/monkeyWie/gopeed/cmd/gopeed
|
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
|
||||||
|
flutter build linux
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# name: 'translator'
|
||||||
|
|
||||||
|
# on:
|
||||||
|
# issues:
|
||||||
|
# types: [opened, edited]
|
||||||
|
# issue_comment:
|
||||||
|
# types: [created, edited]
|
||||||
|
# discussion:
|
||||||
|
# types: [created, edited]
|
||||||
|
# discussion_comment:
|
||||||
|
# types: [created, edited]
|
||||||
|
# pull_request_target:
|
||||||
|
# types: [opened, edited]
|
||||||
|
# pull_request_review_comment:
|
||||||
|
# types: [created, edited]
|
||||||
|
|
||||||
|
# jobs:
|
||||||
|
# translate:
|
||||||
|
# permissions:
|
||||||
|
# issues: write
|
||||||
|
# discussions: write
|
||||||
|
# pull-requests: write
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: lizheming/[email protected]
|
||||||
|
# env:
|
||||||
|
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# with:
|
||||||
|
# IS_MODIFY_TITLE: true
|
||||||
|
# APPEND_TRANSLATION: true
|
||||||
+7
-2
@@ -4,7 +4,7 @@
|
|||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
ui/flutter/dist/
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
@@ -21,7 +21,12 @@ bin/
|
|||||||
|
|
||||||
*.data
|
*.data
|
||||||
*.db
|
*.db
|
||||||
|
*.log
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
dist/
|
node_modules/
|
||||||
|
cmd/web/dist/
|
||||||
|
.test_storage/
|
||||||
|
.test_download/
|
||||||
|
/extensions
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Gopeed contributors guide
|
||||||
|
|
||||||
|
Firstly, thank you for your interest in contributing to Gopeed. This guide will help you better
|
||||||
|
participate in the development of Gopeed.
|
||||||
|
|
||||||
|
## Branch description
|
||||||
|
|
||||||
|
This project only has one main branch, namely the `main` branch. If you want to participate in the
|
||||||
|
development of Gopeed, please fork this project first, and then develop in your fork project. After
|
||||||
|
development is completed, submit a PR to this project and merge it into the `main` branch.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
It is recommended to develop and debug through the web. First, start the backend service, and start
|
||||||
|
it by the command line `go run cmd/api/main.go`, the default port of the service is `9999`, and then
|
||||||
|
start the front-end flutter project in `debug` mode to run.
|
||||||
|
|
||||||
|
## Translation
|
||||||
|
|
||||||
|
The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n/langs` directory.
|
||||||
|
You only need to add the corresponding language file in this directory.
|
||||||
|
|
||||||
|
Please refer to `en_us.dart` for translation.
|
||||||
|
|
||||||
|
## flutter development
|
||||||
|
|
||||||
|
Don't forget to run`dart format ./ui/flutter`before you commit to keep your code in standard dart format
|
||||||
|
|
||||||
|
Turn on build_runner watcher if you want to edit api/models:
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub run build_runner watch
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Gopeed コントリビューターガイド
|
||||||
|
|
||||||
|
まず最初に、Gopeed への貢献に興味を持っていただきありがとうございます。このガイドは、あなたが Gopeed の
|
||||||
|
開発に参加するための手助けとなるでしょう。
|
||||||
|
|
||||||
|
## ブランチの説明
|
||||||
|
|
||||||
|
このプロジェクトのメインブランチは `main` ブランチのみです。Gopeed の開発に参加したい場合は、
|
||||||
|
まずこのプロジェクトをフォークし、フォークしたプロジェクトで開発を行ってください。開発が完了したら、
|
||||||
|
このプロジェクトに PR を提出し、`main` ブランチにマージしてください。
|
||||||
|
|
||||||
|
## ローカル開発
|
||||||
|
|
||||||
|
開発およびデバッグはウェブ上で行うことを推奨する。まずバックエンドのサービスを起動し、
|
||||||
|
コマンドライン `go run cmd/api/main.go` で起動する。サービスのデフォルトポートは `9999` で、
|
||||||
|
次にフロントエンドの flutter プロジェクトを `debug` モードで起動して実行します。
|
||||||
|
|
||||||
|
## 翻訳
|
||||||
|
|
||||||
|
Gopeed の国際化ファイルは `ui/flutter/lib/i18n/langs` ディレクトリにあります。
|
||||||
|
このディレクトリに対応する言語ファイルを追加するだけでよいです。
|
||||||
|
|
||||||
|
翻訳については `en_us.dart` を参照してください。
|
||||||
|
|
||||||
|
## flutter での開発
|
||||||
|
|
||||||
|
コミットする前に `dart format ./ui/flutter` を実行し、コードを標準の dart フォーマットにしておくことを忘れないでください
|
||||||
|
|
||||||
|
api/models を編集したい場合は build_runner watcher をオンにします:
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub run build_runner watch
|
||||||
|
```
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Gopeed 贡献指南
|
||||||
|
|
||||||
|
首先感谢您对贡献代码感兴趣,这份指南将帮助您更好的参与到 Gopeed 的开发中来。
|
||||||
|
|
||||||
|
## 分支说明
|
||||||
|
|
||||||
|
本项目只有一个主分支,即 `main` 分支,如果您想要参与到 Gopeed 的开发中来,请先 fork 本项目,然后在您的 fork 项目中进行开发,开发完成后再向本项目提交
|
||||||
|
PR,合并到 `main` 分支。
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
建议通过 web 端进行开发调试,首先启动后端服务,通过命令行 `go run cmd/api/main.go` 启动 ,服务启动默认端口为 `9999`,然后以 `debug` 模式启动前端
|
||||||
|
flutter 项目即可运行。
|
||||||
|
|
||||||
|
## 翻译
|
||||||
|
|
||||||
|
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
|
||||||
|
|
||||||
|
请注意以 `en_us.dart` 为参照进行翻译。
|
||||||
|
|
||||||
|
## flutter开发
|
||||||
|
|
||||||
|
每次提交前请务必`dart format ./ui/flutter`
|
||||||
|
|
||||||
|
如果要编辑api/models,请打开build_runner watcher:
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub run build_runner watch
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Gopeed 協助指南
|
||||||
|
|
||||||
|
首先感謝您願意幫助我們改進並優化該項目,這份指南將會幫助您更好的參與 Gopeed 的開發。
|
||||||
|
|
||||||
|
## 分支說明
|
||||||
|
|
||||||
|
本項目只有一個分支,即 `main` 分支,如果您想要參與 Gopeed 的開發,請先 fork 該項目,再在您自己的 fork 中進行開發,開發完成後再開啟PR,以合併至 `main` 分支。
|
||||||
|
|
||||||
|
## 離線開發
|
||||||
|
|
||||||
|
建議使用 web 端進行開發與調試,首先啟動服務,使用指令 `go run cmd/api/main.go` 啟動 ,該服務默認連接埠為 `9999`,接著以 `debug` 模式啟動前端 flutter 項目即可。
|
||||||
|
|
||||||
|
## 翻譯
|
||||||
|
|
||||||
|
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
|
||||||
|
|
||||||
|
|
||||||
|
請以 `en_us.dart` 作為參照。
|
||||||
|
|
||||||
|
## flutter開發
|
||||||
|
|
||||||
|
每次提交PR前請務必執行 `dart format ./ui/flutter`
|
||||||
|
|
||||||
|
如果需要編輯 api/models,請打開build_runner watcher:
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub run build_runner watch
|
||||||
|
```
|
||||||
+8
-6
@@ -1,21 +1,23 @@
|
|||||||
FROM cirrusci/flutter:3.3.7 AS flutter
|
FROM instrumentisto/flutter:3.16.8 AS flutter
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
||||||
RUN flutter pub get
|
RUN flutter pub get
|
||||||
COPY ./ui/flutter ./
|
COPY ./ui/flutter ./
|
||||||
RUN flutter build web
|
RUN flutter build web --web-renderer html
|
||||||
|
|
||||||
FROM golang:1.19.3 AS go
|
FROM golang:1.21.6 AS go
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./go.mod ./go.sum ./
|
COPY ./go.mod ./go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=flutter /app/build/web/* ./cmd/web/dist/
|
COPY --from=flutter /app/build/web ./cmd/web/dist
|
||||||
RUN go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/monkeyWie/gopeed/cmd/web
|
ARG VERSION=dev
|
||||||
|
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=${VERSION}" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
|
||||||
FROM alpine:3.14.2
|
FROM alpine:3.14.2
|
||||||
LABEL maintainer="monkeyWie"
|
LABEL maintainer="monkeyWie"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=go /app/dist/gopeed ./
|
COPY --from=go /app/dist/gopeed ./
|
||||||
|
VOLUME ["/app/storage"]
|
||||||
EXPOSE 9999
|
EXPOSE 9999
|
||||||
ENTRYPOINT ["./gopeed"]
|
ENTRYPOINT ["./gopeed"]
|
||||||
@@ -1,48 +1,100 @@
|
|||||||
[](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/monkeyWie/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/monkeyWie/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which supports (HTTP, BitTorrent, Magnet) protocol downloads and supports all platforms.
|
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).
|
||||||
|
|
||||||
## Installation
|
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
|
||||||
|
|
||||||
**Supported platforms**
|
## Download
|
||||||
|
|
||||||
- [x] windows
|
<table>
|
||||||
- [x] macos
|
<thead>
|
||||||
- [x] linux
|
<tr>
|
||||||
- [x] android
|
<th>Platform</th>
|
||||||
- [ ] ios
|
<th>Package Type</th>
|
||||||
- [x] web
|
<th>Download Link</th>
|
||||||
- [x] docker
|
</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-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<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.deb">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">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>
|
||||||
|
|
||||||
[To Release](https://github.com/monkeyWie/gopeed/releases/latest)
|
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
|
||||||
|
|
||||||
> 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
|
||||||
|
|
||||||
use `go install`:
|
use `go install`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/monkeyWie/gopeed/cmd/gopeed
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
## Donate
|
||||||
|
|
||||||
```bash
|
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
|
||||||
docker run liwei2633/gopeed:latest -v /path/to/download:/download -p 9999:9999
|
|
||||||
```
|
|
||||||
|
|
||||||
When the docker container is running, you can access the web page through `http://localhost:9999`.
|
|
||||||
> Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host.
|
|
||||||
|
|
||||||
## Showcase
|
## Showcase
|
||||||
|
|
||||||
@@ -56,27 +108,31 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.21+
|
||||||
2. Flutter 3.0+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:monkeyWie/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
#### Desktop
|
#### Desktop
|
||||||
|
|
||||||
First, you need to configure the environment according to the [flutter desktop official website document](https://docs.flutter.dev/development/platform-integration/desktop), and then you need to prepare the `cgo` environment, which can be searched for yourself.
|
First, you need to configure the environment according to the official [Flutter desktop website documention](https://docs.flutter.dev/development/platform-integration/desktop), then you will need to ensure the cgo environment is set up accordingly. For detailed instructions on setting up the cgo environment, please refer to relevant resources available online.
|
||||||
|
|
||||||
command:
|
command:
|
||||||
|
|
||||||
- windows
|
- windows
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
|
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
|
cd ui/flutter
|
||||||
flutter build windows
|
flutter build windows
|
||||||
```
|
```
|
||||||
@@ -84,7 +140,7 @@ flutter build windows
|
|||||||
- macos
|
- macos
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
|
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
|
cd ui/flutter
|
||||||
flutter build macos
|
flutter build macos
|
||||||
```
|
```
|
||||||
@@ -92,7 +148,7 @@ flutter build macos
|
|||||||
- linux
|
- linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop
|
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 linux
|
||||||
```
|
```
|
||||||
@@ -103,6 +159,7 @@ Same as before, you also need to prepare the `cgo` environment, and then install
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
gomobile init
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,14 +168,20 @@ 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/monkeyWie/gopeed/bind/mobile
|
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
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Web
|
- ios
|
||||||
|
|
||||||
Web platform communicates directly with the backend http server, no additional environment is required.
|
```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:
|
command:
|
||||||
|
|
||||||
@@ -126,9 +189,22 @@ command:
|
|||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web
|
flutter build web
|
||||||
cd ../../
|
cd ../../
|
||||||
|
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/monkeyWie/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
````
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/donate.html)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
||||||
|
|
||||||
|
## はじめに
|
||||||
|
|
||||||
|
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
|
||||||
|
|
||||||
|
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
|
||||||
|
|
||||||
|
## インストール
|
||||||
|
|
||||||
|
<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-windows-amd64-portable.zip">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<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.deb">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">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>
|
||||||
|
|
||||||
|
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||||
|
|
||||||
|
## 寄付
|
||||||
|
|
||||||
|
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
|
||||||
|
|
||||||
|
## ショーケース
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 開発
|
||||||
|
|
||||||
|
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
|
||||||
|
|
||||||
|
> フロントコードは `ui/flutter` ディレクトリにあります。
|
||||||
|
|
||||||
|
### 環境
|
||||||
|
|
||||||
|
1. Go 言語 1.21+
|
||||||
|
2. Flutter 3.16+
|
||||||
|
|
||||||
|
### クローン
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### コントリビュート
|
||||||
|
|
||||||
|
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
|
||||||
|
|
||||||
|
### ビルド
|
||||||
|
|
||||||
|
#### デスクトップ
|
||||||
|
|
||||||
|
まず、[flutter デスクトップ公式サイトドキュメント](https://docs.flutter.dev/development/platform-integration/desktop)に従って環境を設定し、自分で検索できる `cgo` 環境を用意します。
|
||||||
|
|
||||||
|
コマンド:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### モバイル
|
||||||
|
|
||||||
|
先ほどと同じように、`cgo` 環境を準備し、`gomobile` をインストールする必要があります:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
```
|
||||||
|
|
||||||
|
コマンド:
|
||||||
|
|
||||||
|
- android
|
||||||
|
|
||||||
|
```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
|
||||||
|
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
|
||||||
|
|
||||||
|
コマンド:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 感謝
|
||||||
|
|
||||||
|
### コントリビューター
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
[GPLv3](LICENSE)
|
||||||
+111
-35
@@ -1,48 +1,100 @@
|
|||||||
[](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/monkeyWie/gopeed)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
[](https://github.com/monkeyWie/gopeed/releases)
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/zh/donate.html)
|
||||||
[](https://discord.gg/ZUJqJrwCGB)
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md)
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
||||||
|
|
||||||
## 介绍
|
## 介绍
|
||||||
|
|
||||||
Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
|
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/)
|
||||||
|
|
||||||
## 下载
|
## 下载
|
||||||
|
|
||||||
**已支持平台**
|
<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-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<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.deb">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">前往</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
|
|
||||||
- [ ] ios
|
|
||||||
- [x] web
|
|
||||||
- [x] docker
|
|
||||||
|
|
||||||
[点击前往](https://github.com/monkeyWie/gopeed/releases/latest)
|
|
||||||
|
|
||||||
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/gopeed.app` 命令
|
|
||||||
|
|
||||||
### 命令行工具
|
### 命令行工具
|
||||||
|
|
||||||
使用`go install`安装:
|
使用`go install`安装:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/monkeyWie/gopeed/cmd/gopeed
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
## 赞助
|
||||||
|
|
||||||
```bash
|
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
|
||||||
docker run liwei2633/gopeed:latest -v /path/to/download:/download -p 9999:9999
|
|
||||||
```
|
|
||||||
|
|
||||||
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
|
||||||
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
|
|
||||||
|
|
||||||
## 界面展示
|
## 界面展示
|
||||||
|
|
||||||
@@ -56,15 +108,19 @@ docker run liwei2633/gopeed:latest -v /path/to/download:/download -p 9999:9999
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
1. Golang 1.19+
|
1. Golang 1.21+
|
||||||
2. Flutter 3.0+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆项目
|
### 克隆项目
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone [email protected]:monkeyWie/gopeed.git
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 贡献代码
|
||||||
|
|
||||||
|
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
|
||||||
|
|
||||||
### 编译
|
### 编译
|
||||||
|
|
||||||
#### 桌面端
|
#### 桌面端
|
||||||
@@ -76,7 +132,7 @@ git clone [email protected]:monkeyWie/gopeed.git
|
|||||||
- windows
|
- windows
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop
|
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
|
cd ui/flutter
|
||||||
flutter build windows
|
flutter build windows
|
||||||
```
|
```
|
||||||
@@ -84,7 +140,7 @@ flutter build windows
|
|||||||
- macos
|
- macos
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
|
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
|
cd ui/flutter
|
||||||
flutter build macos
|
flutter build macos
|
||||||
```
|
```
|
||||||
@@ -92,7 +148,7 @@ flutter build macos
|
|||||||
- linux
|
- linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop
|
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 linux
|
||||||
```
|
```
|
||||||
@@ -103,6 +159,7 @@ flutter build linux
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
gomobile init
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,14 +168,20 @@ 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/monkeyWie/gopeed/bind/mobile
|
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
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Web 端(推荐本地调试使用)
|
- ios
|
||||||
|
|
||||||
Web 端直接与后端 http 服务通讯,不需要额外准备环境。
|
```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 端
|
||||||
|
|
||||||
构建命令:
|
构建命令:
|
||||||
|
|
||||||
@@ -126,10 +189,23 @@ Web 端直接与后端 http 服务通讯,不需要额外准备环境。
|
|||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web
|
flutter build web
|
||||||
cd ../../
|
cd ../../
|
||||||
|
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/monkeyWie/gopeed/cmd/web
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 感谢
|
||||||
|
|
||||||
|
### 贡献者
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
## 开源许可
|
## 开源许可
|
||||||
|
|
||||||
基于 [GPLv3](LICENSE) 协议开源。
|
基于 [GPLv3](LICENSE) 协议开源。
|
||||||
+211
@@ -0,0 +1,211 @@
|
|||||||
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://docs.gopeed.com/donate.html)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[](https://ko-fi.com/R6R6IJGN6)
|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
|
||||||
|
|
||||||
|
## 簡介
|
||||||
|
|
||||||
|
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
|
||||||
|
|
||||||
|
前往 ✈ [主頁](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-windows-amd64-portable.zip">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan=4>Linux</td>
|
||||||
|
<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.deb">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>AppImage</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>Portable TAR</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64-portable.tar.gz">前往</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>
|
||||||
|
|
||||||
|
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
|
### 使用CLI安裝
|
||||||
|
|
||||||
|
使用`go install`安裝:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 贊助
|
||||||
|
|
||||||
|
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
|
||||||
|
|
||||||
|
## 軟體介面
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 開發
|
||||||
|
|
||||||
|
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
|
||||||
|
|
||||||
|
> 前端代碼位於`ui/flutter`目錄內。
|
||||||
|
|
||||||
|
### 開發環境
|
||||||
|
|
||||||
|
1. Golang 1.21+
|
||||||
|
2. Flutter 3.16+
|
||||||
|
|
||||||
|
### 克隆項目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 協助開發
|
||||||
|
|
||||||
|
請參考[協助指南](CONTRIBUTING_zh-TW.md)
|
||||||
|
|
||||||
|
### 編譯
|
||||||
|
|
||||||
|
#### 桌面端
|
||||||
|
|
||||||
|
首先需要按照[flutter desktop 官方文檔](https://docs.flutter.dev/development/platform-integration/desktop)配置開發環境,並準備好`cgo`環境,具體方法可以自行搜索。
|
||||||
|
|
||||||
|
組建指令:
|
||||||
|
|
||||||
|
- 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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 移動設備
|
||||||
|
|
||||||
|
需要`cgo`環境,並安裝`gomobile`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
```
|
||||||
|
|
||||||
|
組建指令:
|
||||||
|
|
||||||
|
- android
|
||||||
|
|
||||||
|
```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
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 網頁端
|
||||||
|
|
||||||
|
組建指令:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 感謝
|
||||||
|
|
||||||
|
### 貢獻者
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
|
## 軟體許可
|
||||||
|
|
||||||
|
該軟體遵循 [GPLv3](LICENSE) 。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg height="105" viewBox="0 0 105 105" width="105" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="99.03" x2="61.54" y1="59.18" y2="90.91"><stop offset="0" stop-color="#087cfa"/><stop offset=".02" stop-color="#0d7bfa"/><stop offset=".37" stop-color="#5566f9"/><stop offset=".66" stop-color="#8a57f8"/><stop offset=".88" stop-color="#ab4ef7"/><stop offset="1" stop-color="#b74af7"/></linearGradient><linearGradient id="b" x1="37.54" x2="60.33" xlink:href="#a" y1="34.26" y2="8.33"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="17.84" x2="87.09" y1="88.36" y2="43.86"><stop offset="0" stop-color="#087cfa"/><stop offset=".1" stop-color="#1598d3"/><stop offset=".22" stop-color="#23b6aa"/><stop offset=".35" stop-color="#2dcc8b"/><stop offset=".46" stop-color="#35dd74"/><stop offset=".57" stop-color="#39e767"/><stop offset=".67" stop-color="#3bea62"/></linearGradient><path d="m89.06 41.63 11.44 25.37-19.58 33.5-12.06-29.28z" fill="url(#a)"/><path d="m68.86 71.22 12.06 29.28-30.34-10.43z" fill="#b74af7"/><path d="m72.13 31.61-6.22-27.11h-34.49l-26.92 41.18 7.79 18.92-7.79 17.27 55.75-47.43z" fill="url(#b)"/><path d="m100.5 24.88-40.25 9.56-55.75 47.43 35.96 18.63 28.4-29.28z" fill="url(#c)"/><path d="m22.5 22.5h60v60h-60z"/><g fill="#fff"><path d="m28.13 71.25h22.5v3.75h-22.5z"/><path d="m26.44 42.81v-.07a12.7 12.7 0 0 1 12.97-12.74 13.62 13.62 0 0 1 9.73 3.39l-3.43 4.14a9.13 9.13 0 0 0 -6.47-2.53c-4 0-7.11 3.5-7.11 7.7v.07c0 4.53 3.11 7.85 7.5 7.85a8.67 8.67 0 0 0 5.12-1.49v-3.56h-5.48v-4.7h10.73v10.74a16.07 16.07 0 0 1 -10.57 3.93c-7.69 0-12.99-5.41-12.99-12.73z"/><path d="m52.3 42.81v-.07a12.79 12.79 0 0 1 13.19-12.74c7.63 0 13.11 5.69 13.11 12.72v.07a12.79 12.79 0 0 1 -13.18 12.8c-7.64-.05-13.12-5.74-13.12-12.78zm20.61 0v-.07a7.51 7.51 0 0 0 -7.49-7.74 7.4 7.4 0 0 0 -7.42 7.67v.07a7.52 7.52 0 0 0 7.5 7.78 7.4 7.4 0 0 0 7.41-7.71z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -2,8 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/monkeyWie/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/monkeyWie/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -16,7 +17,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
}).
|
}).
|
||||||
Create(&base.Options{
|
Create(&base.Options{
|
||||||
Connections: 8,
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 8,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package main
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/monkeyWie/gopeed/pkg/rest"
|
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||||
"github.com/monkeyWie/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {}
|
func main() {}
|
||||||
@@ -15,6 +15,7 @@ func Start(cfg *C.char) (int, *C.char) {
|
|||||||
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
||||||
return 0, C.CString(err.Error())
|
return 0, C.CString(err.Error())
|
||||||
}
|
}
|
||||||
|
config.ProductionMode = true
|
||||||
realPort, err := rest.Start(&config)
|
realPort, err := rest.Start(&config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, C.CString(err.Error())
|
return 0, C.CString(err.Error())
|
||||||
|
|||||||
+3
-2
@@ -4,8 +4,8 @@ package libgopeed
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/monkeyWie/gopeed/pkg/rest"
|
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||||
"github.com/monkeyWie/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Start(cfg string) (int, error) {
|
func Start(cfg string) (int, error) {
|
||||||
@@ -13,6 +13,7 @@ func Start(cfg string) (int, error) {
|
|||||||
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
|
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
config.ProductionMode = true
|
||||||
return rest.Start(&config)
|
return rest.Start(&config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// only for local development
|
||||||
|
func main() {
|
||||||
|
cfg := &model.StartConfig{
|
||||||
|
Network: "tcp",
|
||||||
|
Address: "127.0.0.1:9999",
|
||||||
|
Storage: model.StorageBolt,
|
||||||
|
WebEnable: true,
|
||||||
|
}
|
||||||
|
cmd.Start(cfg)
|
||||||
|
}
|
||||||
File renamed without changes.
+1
-1
@@ -25,7 +25,7 @@ func parse() *args {
|
|||||||
if len(t) > 0 {
|
if len(t) > 0 {
|
||||||
args.url = t[0]
|
args.url = t[0]
|
||||||
} else {
|
} else {
|
||||||
gPrintln("missing url parameter")
|
gPrintln("missing url parameter, for example: gopeed https://www.google.com or gopeed bt.torrent or gopeed magnet:?xt=urn:btih:...")
|
||||||
gPrintln("try 'gopeed -h' for more information")
|
gPrintln("try 'gopeed -h' for more information")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-8
@@ -2,9 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/monkeyWie/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/monkeyWie/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/monkeyWie/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -15,6 +17,7 @@ func main() {
|
|||||||
args := parse()
|
args := parse()
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
_, err := download.Boot().
|
_, err := download.Boot().
|
||||||
URL(args.url).
|
URL(args.url).
|
||||||
Listener(func(event *download.Event) {
|
Listener(func(event *download.Event) {
|
||||||
@@ -39,26 +42,37 @@ func main() {
|
|||||||
}
|
}
|
||||||
}).
|
}).
|
||||||
Create(&base.Options{
|
Create(&base.Options{
|
||||||
Path: *args.dir,
|
Path: *args.dir,
|
||||||
Connections: *args.connections,
|
Extra: http.OptsExtra{Connections: *args.connections},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
wg.Add(1)
|
printProgress(emptyTask, "downloading...")
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lastLineLen = 0
|
lastLineLen = 0
|
||||||
sb = new(strings.Builder)
|
sb = new(strings.Builder)
|
||||||
|
emptyTask = &download.Task{
|
||||||
|
Progress: &download.Progress{},
|
||||||
|
Meta: &fetcher.FetcherMeta{
|
||||||
|
Res: &base.Resource{},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func printProgress(task *download.Task, title string) {
|
func printProgress(task *download.Task, title string) {
|
||||||
rate := float64(task.Progress.Downloaded) / float64(task.Res.Size)
|
var rate float64
|
||||||
|
if task.Meta.Res.Size <= 0 {
|
||||||
|
rate = 0
|
||||||
|
} else {
|
||||||
|
rate = float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
|
||||||
|
}
|
||||||
completeWidth := int(progressWidth * rate)
|
completeWidth := int(progressWidth * rate)
|
||||||
speed := util.ByteFmt(task.Progress.Speed)
|
speed := util.ByteFmt(task.Progress.Speed)
|
||||||
totalSize := util.ByteFmt(task.Res.Size)
|
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
||||||
sb.WriteString(fmt.Sprintf("\r%s [", title))
|
sb.WriteString(fmt.Sprintf("\r%s [", title))
|
||||||
for i := 0; i < progressWidth; i++ {
|
for i := 0; i < progressWidth; i++ {
|
||||||
if i < completeWidth {
|
if i < completeWidth {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed banner.txt
|
||||||
|
var banner string
|
||||||
|
|
||||||
|
func Start(cfg *model.StartConfig) {
|
||||||
|
fmt.Println(banner)
|
||||||
|
srv, listener, err := rest.BuildServer(cfg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
downloadCfg, err := rest.Downloader.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if downloadCfg.FirstLoad {
|
||||||
|
// Set default download dir to user download dir
|
||||||
|
userDir, err := os.UserHomeDir()
|
||||||
|
if err == nil {
|
||||||
|
downloadDir := filepath.Join(userDir, "Downloads")
|
||||||
|
downloadCfg.DownloadDir = downloadDir
|
||||||
|
rest.Downloader.PutConfig(downloadCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
||||||
|
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Address *string `json:"address"`
|
||||||
|
Port *int `json:"port"`
|
||||||
|
Username *string `json:"username"`
|
||||||
|
Password *string `json:"password"`
|
||||||
|
ApiToken *string `json:"apiToken"`
|
||||||
|
|
||||||
|
configPath *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse() *args {
|
||||||
|
var cliArgs args
|
||||||
|
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||||
|
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||||
|
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
||||||
|
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.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// args priority: config file > cli args
|
||||||
|
cfgArgs := loadConfig(*cliArgs.configPath)
|
||||||
|
if cfgArgs.Address == nil {
|
||||||
|
cfgArgs.Address = cliArgs.Address
|
||||||
|
}
|
||||||
|
if cfgArgs.Port == nil {
|
||||||
|
cfgArgs.Port = cliArgs.Port
|
||||||
|
}
|
||||||
|
if cfgArgs.Username == nil {
|
||||||
|
cfgArgs.Username = cliArgs.Username
|
||||||
|
}
|
||||||
|
if cfgArgs.Password == nil {
|
||||||
|
cfgArgs.Password = cliArgs.Password
|
||||||
|
}
|
||||||
|
if cfgArgs.ApiToken == nil {
|
||||||
|
cfgArgs.ApiToken = cliArgs.ApiToken
|
||||||
|
}
|
||||||
|
return cfgArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(path string) *args {
|
||||||
|
var args args
|
||||||
|
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
if err != nil {
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
path = filepath.Join(dir, path)
|
||||||
|
}
|
||||||
|
file, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
fmt.Println("config file read failed, reason:" + err.Error())
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(file, &args); err != nil {
|
||||||
|
fmt.Println("config file parse failed, reason:" + err.Error())
|
||||||
|
return &args
|
||||||
|
}
|
||||||
|
fmt.Printf("config file loaded: %s\n", path)
|
||||||
|
return &args
|
||||||
|
}
|
||||||
+31
-17
@@ -6,15 +6,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/monkeyWie/gopeed/pkg/rest"
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
"github.com/monkeyWie/gopeed/pkg/rest/model"
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed banner.txt
|
|
||||||
var banner string
|
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
var dist embed.FS
|
var dist embed.FS
|
||||||
|
|
||||||
@@ -23,20 +21,36 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
cfg := &model.StartConfig{
|
|
||||||
Network: "tcp",
|
args := parse()
|
||||||
Address: "127.0.0.1:9999",
|
var webBasicAuth *model.WebBasicAuth
|
||||||
Storage: model.StorageBolt,
|
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
||||||
WebEnable: true,
|
webBasicAuth = &model.WebBasicAuth{
|
||||||
WebFS: sub,
|
Username: *args.Username,
|
||||||
|
Password: *args.Password,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Println(banner)
|
|
||||||
srv, listener, err := rest.BuildServer(cfg)
|
exe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
|
dir := filepath.Dir(exe)
|
||||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
|
||||||
panic(err)
|
cfg := &model.StartConfig{
|
||||||
|
Network: "tcp",
|
||||||
|
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||||
|
Storage: model.StorageBolt,
|
||||||
|
StorageDir: filepath.Join(dir, "storage"),
|
||||||
|
ApiToken: *args.ApiToken,
|
||||||
|
ProductionMode: true,
|
||||||
|
WebEnable: true,
|
||||||
|
WebFS: sub,
|
||||||
|
WebBasicAuth: webBasicAuth,
|
||||||
}
|
}
|
||||||
|
cmd.Start(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNotBlank(str *string) bool {
|
||||||
|
return str != nil && *str != ""
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gopeed:
|
||||||
|
container_name: gopeed
|
||||||
|
ports:
|
||||||
|
- "9999:9999" # HTTP port (host:container)
|
||||||
|
image: liwei2633/gopeed
|
||||||
|
volumes:
|
||||||
|
- ~/gopeed/Downloads:/root/Downloads # mount download path
|
||||||
|
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
||||||
|
restart: unless-stopped
|
||||||
@@ -1,76 +1,117 @@
|
|||||||
module github.com/monkeyWie/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.19
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/torrent v1.46.0
|
github.com/anacrolix/missinggo/v2 v2.7.3
|
||||||
|
github.com/anacrolix/torrent v1.53.3
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
|
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
|
||||||
|
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
|
||||||
|
github.com/go-git/go-git/v5 v5.8.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
|
||||||
go.etcd.io/bbolt v1.3.6
|
github.com/pkg/errors v0.9.1
|
||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
github.com/rs/zerolog v1.31.0
|
||||||
|
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
||||||
|
go.etcd.io/bbolt v1.3.8
|
||||||
|
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||||
|
golang.org/x/sync v0.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
||||||
|
github.com/RoaringBitmap/roaring v1.7.0 // 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/anacrolix/chansync v0.3.0 // indirect
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51 // indirect
|
github.com/anacrolix/chansync v0.4.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.2.1 // indirect
|
github.com/anacrolix/dht/v2 v2.21.0 // indirect
|
||||||
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60 // indirect
|
github.com/anacrolix/envpprof v1.3.0 // indirect
|
||||||
github.com/anacrolix/go-libutp v1.2.0 // indirect
|
github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
|
||||||
github.com/anacrolix/log v0.13.1 // indirect
|
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||||
|
github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 // indirect
|
||||||
github.com/anacrolix/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/missinggo/v2 v2.7.0 // indirect
|
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||||
github.com/anacrolix/multiless v0.2.1-0.20211218050420-533661eef5dc // indirect
|
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
||||||
github.com/anacrolix/stm v0.3.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.4.0 // indirect
|
github.com/anacrolix/sync v0.5.1 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
github.com/anacrolix/upnp v0.1.3 // 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.0 // indirect
|
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // 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/dustin/go-humanize v1.0.0 // indirect
|
github.com/dlclark/regexp2 v1.10.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.1.0 // 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/google/btree v1.0.1 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||||
github.com/gorilla/handlers v1.5.1 // indirect
|
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/go-llsqlite/crawshaw v0.5.0 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
github.com/lispad/go-generics-tools v1.0.0 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/google/btree v1.1.2 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||||
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.1 // indirect
|
||||||
|
github.com/huandu/xstrings v1.4.0 // indirect
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // 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/pion/datachannel v1.5.5 // indirect
|
||||||
github.com/pion/dtls/v2 v2.1.2 // indirect
|
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||||
github.com/pion/ice/v2 v2.1.20 // indirect
|
github.com/pion/ice/v2 v2.3.12 // indirect
|
||||||
github.com/pion/interceptor v0.1.7 // indirect
|
github.com/pion/interceptor v0.1.25 // 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 v0.0.9 // 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.13 // indirect
|
||||||
github.com/pion/rtp v1.7.4 // indirect
|
github.com/pion/rtp v1.8.3 // indirect
|
||||||
github.com/pion/sctp v1.8.2 // indirect
|
github.com/pion/sctp v1.8.9 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.4 // indirect
|
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.5 // indirect
|
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||||
github.com/pion/stun v0.3.5 // indirect
|
github.com/pion/stun v0.6.1 // indirect
|
||||||
github.com/pion/transport v0.13.0 // indirect
|
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.6 // indirect
|
github.com/pion/turn/v2 v2.1.4 // indirect
|
||||||
github.com/pion/udp v0.1.1 // indirect
|
github.com/pion/webrtc/v3 v3.2.24 // indirect
|
||||||
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/tidwall/btree v1.3.1 // indirect
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835 // indirect
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
|
github.com/tidwall/btree v1.7.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
|
||||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 // indirect
|
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||||
|
golang.org/x/crypto v0.18.0 // indirect
|
||||||
|
golang.org/x/mod v0.14.0 // indirect
|
||||||
|
golang.org/x/net v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
golang.org/x/tools v0.17.0 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.40.6 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/sqlite v1.28.0 // indirect
|
||||||
|
zombiezen.com/go/sqlite v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,30 +1,41 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller interface {
|
type Controller struct {
|
||||||
Touch(name string, size int64) (file *os.File, err error)
|
GetConfig func(v any) bool
|
||||||
|
ProxyUrl *url.URL
|
||||||
|
FileController
|
||||||
//ContextDialer() (proxy.Dialer, error)
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultController struct {
|
type FileController interface {
|
||||||
|
Touch(name string, size int64) (file *os.File, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController() *DefaultController {
|
type DefaultFileController struct {
|
||||||
return &DefaultController{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultController) Touch(name string, size int64) (file *os.File, err error) {
|
func NewController() *Controller {
|
||||||
|
return &Controller{
|
||||||
|
FileController: &DefaultFileController{},
|
||||||
|
GetConfig: func(v any) bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DefaultFileController) Touch(name string, size int64) (file *os.File, err error) {
|
||||||
dir := filepath.Dir(name)
|
dir := filepath.Dir(name)
|
||||||
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, err = os.Create(name)
|
file, err = os.Create(name)
|
||||||
if err != nil && !errors.Is(err, os.ErrExist) {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
|
|||||||
+71
-22
@@ -1,44 +1,93 @@
|
|||||||
package fetcher
|
package fetcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/monkeyWie/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/monkeyWie/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 对应协议的下载支持
|
// Fetcher defines the interface for a download protocol.
|
||||||
|
// One fetcher for each download task
|
||||||
type Fetcher interface {
|
type Fetcher interface {
|
||||||
Setup(ctl controller.Controller) error
|
// Name return the name of the protocol.
|
||||||
// 解析请求
|
Name() string
|
||||||
Resolve(req *base.Request) (res *base.Resource, err error)
|
|
||||||
// 创建任务
|
|
||||||
Create(res *base.Resource, opts *base.Options) (err error)
|
|
||||||
Start() (err error)
|
|
||||||
Pause() (err error)
|
|
||||||
Continue() (err error)
|
|
||||||
Close() (err error)
|
|
||||||
|
|
||||||
// 获取任务各个文件下载进度
|
Setup(ctl *controller.Controller)
|
||||||
|
// Resolve resource info from request
|
||||||
|
Resolve(req *base.Request) error
|
||||||
|
// Create ready to download, but not started
|
||||||
|
Create(opts *base.Options) error
|
||||||
|
Start() error
|
||||||
|
Pause() error
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// Stats refreshes health statistics and returns the latest information
|
||||||
|
Stats() any
|
||||||
|
// Meta returns the meta information of the download.
|
||||||
|
Meta() *FetcherMeta
|
||||||
|
// Progress returns the progress of the download.
|
||||||
Progress() Progress
|
Progress() Progress
|
||||||
// 该方法会一直阻塞,直到任务下载结束
|
// Wait for the download to complete, this method will block until the download is done.
|
||||||
Wait() (err error)
|
Wait() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetcherMeta defines the meta information of a fetcher.
|
||||||
|
type FetcherMeta struct {
|
||||||
|
Req *base.Request `json:"req"`
|
||||||
|
Res *base.Resource `json:"res"`
|
||||||
|
Opts *base.Options `json:"opts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FolderPath return the folder path of the meta info.
|
||||||
|
func (m *FetcherMeta) FolderPath() string {
|
||||||
|
// check if rename folder
|
||||||
|
folder := m.Res.Name
|
||||||
|
if m.Opts.Name != "" {
|
||||||
|
folder = m.Opts.Name
|
||||||
|
}
|
||||||
|
return path.Join(m.Opts.Path, folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SingleFilepath return the single file path of the meta info.
|
||||||
|
func (m *FetcherMeta) SingleFilepath() string {
|
||||||
|
// check if rename file
|
||||||
|
file := m.Res.Files[0]
|
||||||
|
fileName := file.Name
|
||||||
|
if m.Opts.Name != "" {
|
||||||
|
fileName = m.Opts.Name
|
||||||
|
}
|
||||||
|
return path.Join(m.Opts.Path, file.Path, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootDirPath return the root dir path of the task file.
|
||||||
|
func (m *FetcherMeta) RootDirPath() string {
|
||||||
|
if m.Res.Name != "" {
|
||||||
|
return m.FolderPath()
|
||||||
|
} else {
|
||||||
|
return m.Opts.Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetcherBuilder defines the interface for a fetcher builder.
|
||||||
type FetcherBuilder interface {
|
type FetcherBuilder interface {
|
||||||
|
// Schemes returns the schemes supported by the fetcher.
|
||||||
Schemes() []string
|
Schemes() []string
|
||||||
|
// Build returns a new fetcher.
|
||||||
Build() Fetcher
|
Build() Fetcher
|
||||||
|
|
||||||
// Store 存储任务
|
// Store fetcher
|
||||||
Store(fetcher Fetcher) (any, error)
|
Store(fetcher Fetcher) (any, error)
|
||||||
// Restore 恢复任务
|
// Restore fetcher
|
||||||
Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) Fetcher)
|
Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultFetcher struct {
|
type DefaultFetcher struct {
|
||||||
Ctl controller.Controller
|
Ctl *controller.Controller
|
||||||
|
Meta *FetcherMeta
|
||||||
DoneCh chan error
|
DoneCh chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *DefaultFetcher) Setup(ctl controller.Controller) (err error) {
|
func (f *DefaultFetcher) Setup(ctl *controller.Controller) (err error) {
|
||||||
f.Ctl = ctl
|
f.Ctl = ctl
|
||||||
f.DoneCh = make(chan error, 1)
|
f.DoneCh = make(chan error, 1)
|
||||||
return
|
return
|
||||||
@@ -48,10 +97,10 @@ func (f *DefaultFetcher) Wait() (err error) {
|
|||||||
return <-f.DoneCh
|
return <-f.DoneCh
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取任务中各个文件的已下载字节数
|
// Progress is a map of the progress of each file in the torrent.
|
||||||
type Progress []int64
|
type Progress []int64
|
||||||
|
|
||||||
// TotalDownloaded 获取任务总下载字节数
|
// TotalDownloaded returns the total downloaded bytes.
|
||||||
func (p Progress) TotalDownloaded() int64 {
|
func (p Progress) TotalDownloaded() int64 {
|
||||||
total := int64(0)
|
total := int64(0)
|
||||||
for _, d := range p {
|
for _, d := range p {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
zerolog.Logger
|
||||||
|
logFile *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) CLose() {
|
||||||
|
l.logFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogger create a new logger
|
||||||
|
func NewLogger(logFile bool, logPath string) *Logger {
|
||||||
|
var out io.Writer
|
||||||
|
if logFile {
|
||||||
|
// log to file
|
||||||
|
logDir := filepath.Dir(logPath)
|
||||||
|
if err := util.CreateDirIfNotExist(logDir); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
logfile *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
logfile, err = os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
out = logfile
|
||||||
|
} else {
|
||||||
|
out = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := &Logger{}
|
||||||
|
if logFile {
|
||||||
|
logger.logFile = out.(*os.File)
|
||||||
|
}
|
||||||
|
logger.Logger = zerolog.New(zerolog.ConsoleWriter{
|
||||||
|
NoColor: true,
|
||||||
|
Out: out,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
}).With().Timestamp().Logger()
|
||||||
|
return logger
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewLogger(t *testing.T) {
|
||||||
|
logger := NewLogger(false, "")
|
||||||
|
logger.Info().Msg("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewLoggerFile(t *testing.T) {
|
||||||
|
logPath := "./testdata/test.log"
|
||||||
|
logger := NewLogger(true, logPath)
|
||||||
|
defer func() {
|
||||||
|
logger.CLose()
|
||||||
|
os.Remove(logPath)
|
||||||
|
}()
|
||||||
|
logger.Info().Msg("test")
|
||||||
|
|
||||||
|
// read log file, if not exist or empty, test fail.
|
||||||
|
file, err := os.Open(logPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stat.Size() == 0 {
|
||||||
|
t.Fatal("log file is empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
ListenPort int `json:"listenPort"`
|
||||||
|
Trackers []string `json:"trackers"`
|
||||||
|
}
|
||||||
+225
-125
@@ -1,72 +1,166 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
"github.com/anacrolix/torrent/storage"
|
"net/http"
|
||||||
"github.com/monkeyWie/gopeed/internal/controller"
|
|
||||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
|
||||||
"github.com/monkeyWie/gopeed/pkg/base"
|
|
||||||
"github.com/monkeyWie/gopeed/pkg/util"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client *torrent.Client
|
var (
|
||||||
|
client *torrent.Client
|
||||||
|
lock sync.Mutex
|
||||||
|
torrentDirMap = make(map[string]string)
|
||||||
|
ftMap = make(map[string]*fileTorrentImpl)
|
||||||
|
)
|
||||||
|
|
||||||
type Fetcher struct {
|
type Fetcher struct {
|
||||||
Ctl controller.Controller
|
ctl *controller.Controller
|
||||||
|
config *config
|
||||||
|
|
||||||
torrent *torrent.Torrent
|
torrent *torrent.Torrent
|
||||||
res *base.Resource
|
meta *fetcher.FetcherMeta
|
||||||
opts *base.Options
|
|
||||||
|
|
||||||
ready atomic.Bool
|
torrentReady atomic.Bool
|
||||||
progress fetcher.Progress
|
create atomic.Bool
|
||||||
|
progress fetcher.Progress
|
||||||
torrentPaths map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Setup(ctl controller.Controller) (err error) {
|
func (f *Fetcher) Name() string {
|
||||||
f.Ctl = ctl
|
return "bt"
|
||||||
var once sync.Once
|
}
|
||||||
once.Do(func() {
|
|
||||||
cfg := torrent.NewDefaultClientConfig()
|
|
||||||
cfg.ListenPort = 0
|
|
||||||
|
|
||||||
// Support custom download path for each torrent
|
func (f *Fetcher) Setup(ctl *controller.Controller) {
|
||||||
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
|
f.ctl = ctl
|
||||||
if err != nil {
|
if f.meta == nil {
|
||||||
pieceCompletion = storage.NewMapPieceCompletion()
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
|
}
|
||||||
|
exist := f.ctl.GetConfig(&f.config)
|
||||||
|
if !exist {
|
||||||
|
f.config = &config{
|
||||||
|
ListenPort: 0,
|
||||||
|
Trackers: []string{},
|
||||||
}
|
}
|
||||||
clientImplCloser := storage.NewFileOpts(storage.NewFileClientOpts{
|
}
|
||||||
ClientBaseDir: cfg.DataDir,
|
return
|
||||||
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
|
}
|
||||||
if dir, ok := f.torrentPaths[infoHash.String()]; ok {
|
|
||||||
return dir
|
func (f *Fetcher) initClient() (err error) {
|
||||||
}
|
lock.Lock()
|
||||||
return baseDir
|
defer lock.Unlock()
|
||||||
},
|
|
||||||
PieceCompletion: pieceCompletion,
|
if client != nil {
|
||||||
})
|
return
|
||||||
cfg.DefaultStorage = clientImplCloser
|
}
|
||||||
client, err = torrent.NewClient(cfg)
|
|
||||||
if err != nil {
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
|
||||||
|
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
|
||||||
|
cfg.ListenPort = f.config.ListenPort
|
||||||
|
if f.ctl.ProxyUrl != nil {
|
||||||
|
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
|
||||||
|
}
|
||||||
|
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||||
|
ClientBaseDir: cfg.DataDir,
|
||||||
|
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||||
|
if dir, ok := torrentDirMap[infoHash.String()]; ok {
|
||||||
|
ft.setTorrentDir(dir)
|
||||||
|
}
|
||||||
|
ftMap[infoHash.String()] = ft
|
||||||
|
},
|
||||||
|
})
|
||||||
|
client, err = torrent.NewClient(cfg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
|
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.addTorrent(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.updateRes()
|
||||||
|
f.meta.Req = req
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
|
f.create.Store(true)
|
||||||
|
f.meta.Opts = opts
|
||||||
|
if f.meta.Res != nil {
|
||||||
|
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Start() (err error) {
|
||||||
|
if !f.torrentReady.Load() {
|
||||||
|
if err = f.addTorrent(f.meta.Req); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||||
|
ft.setTorrentDir(f.meta.FolderPath())
|
||||||
|
}
|
||||||
|
files := f.torrent.Files()
|
||||||
|
// If the user does not specify the file to download, all files will be downloaded by default
|
||||||
|
if len(f.meta.Opts.SelectFiles) == 0 {
|
||||||
|
f.meta.Opts.SelectFiles = make([]int, len(files))
|
||||||
|
for i := range files {
|
||||||
|
f.meta.Opts.SelectFiles[i] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||||
|
f.torrent.DownloadAll()
|
||||||
|
} else {
|
||||||
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
|
file := files[selectIndex]
|
||||||
|
file.Download()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
f.torrentPaths = make(map[string]string)
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Pause() (err error) {
|
||||||
|
f.torrentReady.Store(false)
|
||||||
|
f.safeDrop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Close() (err error) {
|
||||||
|
f.safeDrop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) safeDrop() {
|
||||||
|
defer func() {
|
||||||
|
// ignore panic
|
||||||
|
_ = recover()
|
||||||
|
}()
|
||||||
|
|
||||||
|
f.torrent.Drop()
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Wait() (err error) {
|
func (f *Fetcher) Wait() (err error) {
|
||||||
for {
|
for {
|
||||||
if f.ready.Load() {
|
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
||||||
done := true
|
done := true
|
||||||
for _, selectIndex := range f.opts.SelectFiles {
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
if file.BytesCompleted() < file.Length() {
|
if file.BytesCompleted() < file.Length() {
|
||||||
done = false
|
done = false
|
||||||
@@ -77,14 +171,14 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
// remove unselected files
|
// remove unselected files
|
||||||
for i, file := range f.torrent.Files() {
|
for i, file := range f.torrent.Files() {
|
||||||
selected := false
|
selected := false
|
||||||
for _, selectIndex := range f.opts.SelectFiles {
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
if i == selectIndex {
|
if i == selectIndex {
|
||||||
selected = true
|
selected = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !selected {
|
if !selected {
|
||||||
util.SafeRemove(filepath.Join(f.opts.Path, file.Path()))
|
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -95,115 +189,108 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
|
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||||
if err = f.addTorrent(req.URL, true); err != nil {
|
return f.meta
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.torrent.Drop()
|
|
||||||
res = &base.Resource{
|
|
||||||
Req: req,
|
|
||||||
Name: f.torrent.Name(),
|
|
||||||
Range: true,
|
|
||||||
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
|
||||||
Extra: map[string]any{
|
|
||||||
"infoHash": f.torrent.InfoHash().String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, file := range f.torrent.Files() {
|
|
||||||
res.Files[i] = &base.FileInfo{
|
|
||||||
Name: filepath.Base(file.DisplayPath()),
|
|
||||||
Path: util.Dir(file.Path()),
|
|
||||||
Size: file.Length(),
|
|
||||||
}
|
|
||||||
res.Size += file.Length()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) {
|
func (f *Fetcher) Stats() any {
|
||||||
f.res = res
|
stats := f.torrent.Stats()
|
||||||
f.opts = opts
|
baseStats := &bt.Stats{
|
||||||
if len(f.opts.SelectFiles) == 0 {
|
TotalPeers: stats.TotalPeers,
|
||||||
f.opts.SelectFiles = make([]int, 0)
|
ActivePeers: stats.ActivePeers,
|
||||||
for i := range f.torrent.Files() {
|
ConnectedSeeders: stats.ConnectedSeeders,
|
||||||
f.opts.SelectFiles = append(f.opts.SelectFiles, i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if f.opts.Path != "" {
|
return baseStats
|
||||||
if extra, ok := res.Extra.(map[string]any); ok {
|
|
||||||
f.torrentPaths[extra["infoHash"].(string)] = f.opts.Path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.progress = make(fetcher.Progress, len(f.opts.SelectFiles))
|
|
||||||
f.ready.Store(false)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Start() (err error) {
|
|
||||||
if err = f.addTorrent(f.res.Req.URL, false); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
files := f.torrent.Files()
|
|
||||||
if len(f.opts.SelectFiles) == len(files) {
|
|
||||||
f.torrent.DownloadAll()
|
|
||||||
} else {
|
|
||||||
for _, selectIndex := range f.opts.SelectFiles {
|
|
||||||
file := files[selectIndex]
|
|
||||||
file.Download()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Pause() (err error) {
|
|
||||||
f.ready.Store(false)
|
|
||||||
f.torrent.Drop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Continue() (err error) {
|
|
||||||
return f.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fetcher) Close() (err error) {
|
|
||||||
f.torrent.Drop()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
if !f.ready.Load() {
|
if !f.torrentReady.Load() {
|
||||||
return f.progress
|
return f.progress
|
||||||
}
|
}
|
||||||
for i := range f.progress {
|
for i := range f.progress {
|
||||||
selectIndex := f.opts.SelectFiles[i]
|
selectIndex := f.meta.Opts.SelectFiles[i]
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
f.progress[i] = file.BytesCompleted()
|
f.progress[i] = file.BytesCompleted()
|
||||||
}
|
}
|
||||||
return f.progress
|
return f.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(url string, resolve bool) (err error) {
|
func (f *Fetcher) updateRes() {
|
||||||
schema := util.ParseSchema(url)
|
res := &base.Resource{
|
||||||
|
Name: f.torrent.Name(),
|
||||||
|
Range: true,
|
||||||
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||||
|
Hash: f.torrent.InfoHash().String(),
|
||||||
|
}
|
||||||
|
f.torrent.PeerConns()
|
||||||
|
for i, file := range f.torrent.Files() {
|
||||||
|
res.Files[i] = &base.FileInfo{
|
||||||
|
Name: filepath.Base(file.DisplayPath()),
|
||||||
|
Path: util.Dir(file.Path()),
|
||||||
|
Size: file.Length(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.CalcSize(nil)
|
||||||
|
f.meta.Res = res
|
||||||
|
if f.meta.Opts != nil {
|
||||||
|
f.meta.Opts.InitSelectFiles(len(res.Files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||||
|
if err = f.initClient(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schema := util.ParseSchema(req.URL)
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(url)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
|
} else if schema == "APPLICATION/X-BITTORRENT" {
|
||||||
|
_, data := util.ParseDataUri(req.URL)
|
||||||
|
buf := bytes.NewBuffer(data)
|
||||||
|
var metaInfo *metainfo.MetaInfo
|
||||||
|
metaInfo, err = metainfo.Load(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
} else {
|
} else {
|
||||||
f.torrent, err = client.AddTorrentFromFile(url)
|
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
<-f.torrent.GotInfo()
|
|
||||||
if resolve {
|
// use map to deduplicate
|
||||||
return
|
trackers := make(map[string]bool)
|
||||||
|
if req.Extra != nil {
|
||||||
|
extra := req.Extra.(*bt.ReqExtra)
|
||||||
|
if len(extra.Trackers) > 0 {
|
||||||
|
for _, tracker := range extra.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
f.ready.Store(true)
|
if len(f.config.Trackers) > 0 {
|
||||||
|
for _, tracker := range f.config.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(trackers) > 0 {
|
||||||
|
announceList := make([][]string, 0)
|
||||||
|
for tracker := range trackers {
|
||||||
|
announceList = append(announceList, []string{tracker})
|
||||||
|
}
|
||||||
|
f.torrent.AddTrackers(announceList)
|
||||||
|
}
|
||||||
|
<-f.torrent.GotInfo()
|
||||||
|
f.torrentReady.Store(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetcherBuilder struct {
|
type FetcherBuilder struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var schemes = []string{"FILE", "MAGNET"}
|
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Schemes() []string {
|
func (fb *FetcherBuilder) Schemes() []string {
|
||||||
return schemes
|
return schemes
|
||||||
@@ -217,6 +304,19 @@ func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) {
|
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
return nil, nil
|
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
|
return &Fetcher{
|
||||||
|
meta: meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse version to bep20 format, fixed length 4, if not enough, fill 0
|
||||||
|
func parseBep20() string {
|
||||||
|
s := strings.ReplaceAll(base.Version, ".", "")
|
||||||
|
if len(s) < 4 {
|
||||||
|
s += strings.Repeat("0", 4-len(s))
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,40 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/monkeyWie/gopeed/internal/controller"
|
"encoding/base64"
|
||||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
"encoding/json"
|
||||||
"github.com/monkeyWie/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||||
|
doResolve(t, buildFetcher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
|
||||||
fetcher := buildFetcher()
|
fetcher := buildFetcher()
|
||||||
res, err := fetcher.Resolve(&base.Request{
|
buf, err := os.ReadFile("./testdata/ubuntu-22.04-live-server-amd64.iso.torrent")
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// convert to data uri
|
||||||
|
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
|
||||||
|
err = fetcher.Resolve(&base.Request{
|
||||||
|
URL: dataUri,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := &base.Resource{
|
want := &base.Resource{
|
||||||
Req: res.Req,
|
|
||||||
Name: "ubuntu-22.04-live-server-amd64.iso",
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
Range: true,
|
Range: true,
|
||||||
@@ -28,12 +44,53 @@ func TestFetcher_Resolve_Torrent(t *testing.T) {
|
|||||||
Size: 1466714112,
|
Size: 1466714112,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Extra: map[string]any{
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
"infoHash": "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, res) {
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Config(t *testing.T) {
|
||||||
|
doResolve(t, buildConfigFetcher(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveWithProxy(t *testing.T) {
|
||||||
|
usr, pwd := "admin", "123"
|
||||||
|
proxyListener := test.StartSocks5Server(usr, pwd)
|
||||||
|
defer proxyListener.Close()
|
||||||
|
|
||||||
|
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.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: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
Size: 1466714112,
|
||||||
|
Range: true,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
Size: 1466714112,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,3 +99,22 @@ func buildFetcher() fetcher.Fetcher {
|
|||||||
fetcher.Setup(controller.NewController())
|
fetcher.Setup(controller.NewController())
|
||||||
return fetcher
|
return fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
|
||||||
|
fetcher := new(FetcherBuilder).Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
mockCfg := config{
|
||||||
|
Trackers: []string{
|
||||||
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
}}
|
||||||
|
newController.GetConfig = func(v any) bool {
|
||||||
|
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
newController.ProxyUrl = proxyUrl
|
||||||
|
fetcher.Setup(newController)
|
||||||
|
return fetcher
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/common"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/segments"
|
||||||
|
)
|
||||||
|
|
||||||
|
// File-based storage for torrents, that isn't yet bound to a particular torrent.
|
||||||
|
type fileClientImpl struct {
|
||||||
|
opts newFileClientOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
type newFileClientOpts struct {
|
||||||
|
// The base directory for all downloads.
|
||||||
|
ClientBaseDir string
|
||||||
|
HandleFileTorrent func(infoHash metainfo.Hash, ft *fileTorrentImpl)
|
||||||
|
FilePathMaker storage.FilePathMaker
|
||||||
|
PieceCompletion storage.PieceCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
|
||||||
|
if opts.FilePathMaker == nil {
|
||||||
|
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
||||||
|
var parts []string
|
||||||
|
if opts.Info.Name != metainfo.NoName {
|
||||||
|
parts = append(parts, opts.Info.Name)
|
||||||
|
}
|
||||||
|
return filepath.Join(append(parts, opts.File.Path...)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.PieceCompletion == nil {
|
||||||
|
ret, err := storage.NewDefaultPieceCompletionForDir(opts.ClientBaseDir)
|
||||||
|
if err != nil {
|
||||||
|
ret = storage.NewMapPieceCompletion()
|
||||||
|
}
|
||||||
|
opts.PieceCompletion = ret
|
||||||
|
}
|
||||||
|
return fileClientImpl{opts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fileClientImpl) Close() error {
|
||||||
|
return fs.opts.PieceCompletion.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
||||||
|
upvertedFiles := info.UpvertedFiles()
|
||||||
|
files := make([]file, 0, len(upvertedFiles))
|
||||||
|
for _, fileInfo := range upvertedFiles {
|
||||||
|
filePath := filepath.Join("", fs.opts.FilePathMaker(storage.FilePathMakerOpts{
|
||||||
|
Info: info,
|
||||||
|
File: &fileInfo,
|
||||||
|
}))
|
||||||
|
f := file{
|
||||||
|
rawPath: filePath,
|
||||||
|
path: filePath,
|
||||||
|
length: fileInfo.Length,
|
||||||
|
}
|
||||||
|
if f.length == 0 {
|
||||||
|
err = CreateNativeZeroLengthFile(f.path)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("creating zero length file: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
t := &fileTorrentImpl{
|
||||||
|
files,
|
||||||
|
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
|
||||||
|
infoHash,
|
||||||
|
fs.opts.PieceCompletion,
|
||||||
|
}
|
||||||
|
fs.opts.HandleFileTorrent(infoHash, t)
|
||||||
|
return storage.TorrentImpl{
|
||||||
|
Piece: t.Piece,
|
||||||
|
Close: t.Close,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type file struct {
|
||||||
|
// The safe, OS-local file path.
|
||||||
|
rawPath string
|
||||||
|
path string
|
||||||
|
length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileTorrentImpl struct {
|
||||||
|
files []file
|
||||||
|
segmentLocater segments.Index
|
||||||
|
infoHash metainfo.Hash
|
||||||
|
completion storage.PieceCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
|
||||||
|
// Create a view onto the file-based torrent storage.
|
||||||
|
_io := fileTorrentImplIO{fts}
|
||||||
|
// Return the appropriate segments of this.
|
||||||
|
return &filePieceImpl{
|
||||||
|
fts,
|
||||||
|
p,
|
||||||
|
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
|
||||||
|
io.NewSectionReader(_io, p.Offset(), p.Length()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) setTorrentDir(dir string) {
|
||||||
|
for i, f := range fts.files {
|
||||||
|
fts.files[i].path = filepath.Join(dir, f.rawPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper to create zero-length files which won't appear for file-orientated storage since no
|
||||||
|
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
|
||||||
|
// caller should make sure the file name provided is safe/sanitized.
|
||||||
|
func CreateNativeZeroLengthFile(name string) error {
|
||||||
|
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||||
|
var f io.Closer
|
||||||
|
f, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
|
||||||
|
type fileTorrentImplIO struct {
|
||||||
|
fts *fileTorrentImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns EOF on short or missing file.
|
||||||
|
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
|
||||||
|
f, err := os.Open(file.path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// File missing is treated the same as a short file.
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
// Limit the read to within the expected bounds of this file.
|
||||||
|
if int64(len(b)) > file.length-off {
|
||||||
|
b = b[:file.length-off]
|
||||||
|
}
|
||||||
|
for off < file.length && len(b) != 0 {
|
||||||
|
n1, err1 := f.ReadAt(b, off)
|
||||||
|
b = b[n1:]
|
||||||
|
n += n1
|
||||||
|
off += int64(n1)
|
||||||
|
if n1 == 0 {
|
||||||
|
err = err1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
|
||||||
|
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
|
||||||
|
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
|
||||||
|
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
|
||||||
|
n += n1
|
||||||
|
b = b[n1:]
|
||||||
|
err = err1
|
||||||
|
return err == nil // && int64(n1) == e.Length
|
||||||
|
})
|
||||||
|
if len(b) != 0 && err == nil {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
|
||||||
|
// log.Printf("write at %v: %v bytes", off, len(p))
|
||||||
|
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
|
||||||
|
name := fst.fts.files[i].path
|
||||||
|
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||||
|
var f *os.File
|
||||||
|
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var n1 int
|
||||||
|
n1, err = f.WriteAt(p[:e.Length], e.Start)
|
||||||
|
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
|
||||||
|
closeErr := f.Close()
|
||||||
|
n += n1
|
||||||
|
p = p[n1:]
|
||||||
|
if err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
if err == nil && int64(n1) != e.Length {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file_misc.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import "github.com/anacrolix/torrent/metainfo"
|
||||||
|
|
||||||
|
type requiredLength struct {
|
||||||
|
fileIndex int
|
||||||
|
length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, fi := range info.UpvertedFiles() {
|
||||||
|
if off >= fi.Length {
|
||||||
|
off -= fi.Length
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n1 := n
|
||||||
|
if off+n1 > fi.Length {
|
||||||
|
n1 = fi.Length - off
|
||||||
|
}
|
||||||
|
ret = append(ret, requiredLength{
|
||||||
|
fileIndex: i,
|
||||||
|
length: off + n1,
|
||||||
|
})
|
||||||
|
n -= n1
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file_piece.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type filePieceImpl struct {
|
||||||
|
*fileTorrentImpl
|
||||||
|
p metainfo.Piece
|
||||||
|
io.WriterAt
|
||||||
|
io.ReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.PieceImpl = (*filePieceImpl)(nil)
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) pieceKey() metainfo.PieceKey {
|
||||||
|
return metainfo.PieceKey{InfoHash: fs.infoHash, Index: fs.p.Index()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) Completion() storage.Completion {
|
||||||
|
c, err := fs.completion.Get(fs.pieceKey())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting piece completion: %s", err)
|
||||||
|
c.Ok = false
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
verified := true
|
||||||
|
if c.Complete {
|
||||||
|
// If it's allegedly complete, check that its constituent files have the necessary length.
|
||||||
|
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
|
||||||
|
s, err := os.Stat(fs.files[fi.fileIndex].path)
|
||||||
|
if err != nil || s.Size() < fi.length {
|
||||||
|
verified = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
// The completion was wrong, fix it.
|
||||||
|
c.Complete = false
|
||||||
|
fs.completion.Set(fs.pieceKey(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) MarkComplete() error {
|
||||||
|
return fs.completion.Set(fs.pieceKey(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) MarkNotComplete() error {
|
||||||
|
return fs.completion.Set(fs.pieceKey(), false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
Connections int `json:"connections"`
|
||||||
|
UseServerCtime bool `json:"useServerCtime"`
|
||||||
|
}
|
||||||
+256
-178
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,21 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/monkeyWie/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/monkeyWie/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/monkeyWie/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/monkeyWie/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFetcher_Resolve(t *testing.T) {
|
func TestFetcher_Resolve(t *testing.T) {
|
||||||
testResolve(test.StartTestFileServer, &base.Resource{
|
testResolve(test.StartTestFileServer, test.BuildName, &base.Resource{
|
||||||
Name: test.BuildName,
|
|
||||||
Size: test.BuildSize,
|
Size: test.BuildSize,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -24,8 +25,17 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
testResolve(test.StartTestChunkedServer, &base.Resource{
|
testResolve(test.StartTestCustomServer, "disposition", &base.Resource{
|
||||||
Name: test.BuildName,
|
Size: test.BuildSize,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: test.BuildName,
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Range: false,
|
Range: false,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
@@ -35,28 +45,38 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
}
|
|
||||||
|
|
||||||
func testResolve(startTestServer func() net.Listener, want *base.Resource, t *testing.T) {
|
|
||||||
listener := startTestServer()
|
|
||||||
defer listener.Close()
|
|
||||||
fetcher := buildFetcher()
|
fetcher := buildFetcher()
|
||||||
res, err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://github.com",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
res.Req = nil
|
if fetcher.Meta().Res.Files[0].Name != "github.com" {
|
||||||
if !reflect.DeepEqual(want, res) {
|
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, "github.com")
|
||||||
t.Errorf("Resolve() got = %v, want %v", res, want)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
||||||
|
listener := startTestServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := buildFetcher()
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "http://" + listener.Addr().String() + "/" + path,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !test.AssertResourceEqual(want, fetcher.meta.Res) {
|
||||||
|
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadNormal(t *testing.T) {
|
func TestFetcher_DownloadNormal(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// 正常下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
downloadNormal(listener, 5, t)
|
downloadNormal(listener, 5, t)
|
||||||
downloadNormal(listener, 8, t)
|
downloadNormal(listener, 8, t)
|
||||||
@@ -66,7 +86,7 @@ func TestFetcher_DownloadNormal(t *testing.T) {
|
|||||||
func TestFetcher_DownloadContinue(t *testing.T) {
|
func TestFetcher_DownloadContinue(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// 暂停继续
|
|
||||||
downloadContinue(listener, 1, t)
|
downloadContinue(listener, 1, t)
|
||||||
downloadContinue(listener, 5, t)
|
downloadContinue(listener, 5, t)
|
||||||
downloadContinue(listener, 8, t)
|
downloadContinue(listener, 8, t)
|
||||||
@@ -74,33 +94,43 @@ func TestFetcher_DownloadContinue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadChunked(t *testing.T) {
|
func TestFetcher_DownloadChunked(t *testing.T) {
|
||||||
listener := test.StartTestChunkedServer()
|
listener := test.StartTestCustomServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
|
downloadNormal(listener, 2, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadPost(t *testing.T) {
|
func TestFetcher_DownloadPost(t *testing.T) {
|
||||||
listener := test.StartTestPostServer()
|
listener := test.StartTestPostServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// post下载
|
|
||||||
downloadPost(listener, 1, t)
|
downloadPost(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadRetry(t *testing.T) {
|
func TestFetcher_DownloadRetry(t *testing.T) {
|
||||||
listener := test.StartTestRetryServer()
|
listener := test.StartTestRetryServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadError(t *testing.T) {
|
func TestFetcher_DownloadError(t *testing.T) {
|
||||||
listener := test.StartTestErrorServer()
|
listener := test.StartTestErrorServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
|
||||||
downloadError(listener, 1, t)
|
downloadError(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadLimit(t *testing.T) {
|
||||||
|
listener := test.StartTestLimitServer(4, 0)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadNormal(listener, 1, t)
|
||||||
|
downloadNormal(listener, 2, t)
|
||||||
|
downloadNormal(listener, 8, 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()
|
||||||
@@ -111,28 +141,89 @@ func TestFetcher_DownloadResume(t *testing.T) {
|
|||||||
downloadResume(listener, 16, t)
|
downloadResume(listener, 16, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadReady(listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) {
|
func TestFetcher_DownloadWithProxy(t *testing.T) {
|
||||||
fetcher = buildFetcher()
|
httpListener := test.StartTestFileServer()
|
||||||
res, err := fetcher.Resolve(&base.Request{
|
defer httpListener.Close()
|
||||||
|
proxyListener := test.StartSocks5Server("", "")
|
||||||
|
defer proxyListener.Close()
|
||||||
|
|
||||||
|
downloadWithProxy(httpListener, proxyListener, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ConfigConnections(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
}), listener, 0, t)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fetcher.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := test.FileMd5(test.BuildFile)
|
||||||
|
got := test.FileMd5(test.DownloadFile)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf("Download() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||||
|
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||||
|
err := f.Resolve(&base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
opts = &base.Options{
|
var extra any = nil
|
||||||
Name: test.DownloadName,
|
if connections > 0 {
|
||||||
Path: test.Dir,
|
extra = http.OptsExtra{
|
||||||
Connections: connections,
|
Connections: connections,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = fetcher.Create(res, opts)
|
opts := &base.Options{
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Path: test.Dir,
|
||||||
|
Extra: extra,
|
||||||
|
}
|
||||||
|
err = f.Create(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
||||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
fetcher := downloadReady(listener, connections, t)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -149,8 +240,8 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadPost(listener net.Listener, connections int, t *testing.T) {
|
func downloadPost(listener net.Listener, connections int, t *testing.T) {
|
||||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
fetcher := downloadReady(listener, connections, t)
|
||||||
fetcher.(*Fetcher).res.Req.Extra = extra{
|
fetcher.Meta().Req.Extra = &http.ReqExtra{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
Header: map[string]string{
|
Header: map[string]string{
|
||||||
"Authorization": "Bearer 123456",
|
"Authorization": "Bearer 123456",
|
||||||
@@ -173,7 +264,7 @@ func downloadPost(listener net.Listener, connections int, t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
||||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
fetcher := downloadReady(listener, connections, t)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -183,7 +274,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 50)
|
time.Sleep(time.Millisecond * 50)
|
||||||
if err := fetcher.Continue(); err != nil {
|
if err := fetcher.Start(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
err = fetcher.Wait()
|
err = fetcher.Wait()
|
||||||
@@ -198,7 +289,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadError(listener net.Listener, connections int, t *testing.T) {
|
func downloadError(listener net.Listener, connections int, t *testing.T) {
|
||||||
fetcher, _, _ := downloadReady(listener, connections, t)
|
fetcher := downloadReady(listener, connections, t)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -210,7 +301,7 @@ func downloadError(listener net.Listener, connections int, t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||||
fetcher, res, opts := downloadReady(listener, connections, t)
|
fetcher := downloadReady(listener, connections, t)
|
||||||
err := fetcher.Start()
|
err := fetcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -226,12 +317,12 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
fetcher.Pause()
|
fetcher.Pause()
|
||||||
|
|
||||||
_, f := fb.Restore()
|
_, f := fb.Restore()
|
||||||
f(res, opts, data)
|
f(fetcher.Meta(), data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher.Setup(controller.NewController())
|
||||||
fetcher.Continue()
|
fetcher.Start()
|
||||||
|
|
||||||
err = fetcher.Wait()
|
err = fetcher.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -244,8 +335,41 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
|
||||||
|
fetcher := downloadReady(httpListener, 4, t)
|
||||||
|
ctl := controller.NewController()
|
||||||
|
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
|
||||||
|
fetcher.Setup(ctl)
|
||||||
|
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 buildFetcher() *Fetcher {
|
func buildFetcher() *Fetcher {
|
||||||
fetcher := new(FetcherBuilder).Build()
|
fetcher := new(FetcherBuilder).Build()
|
||||||
fetcher.Setup(controller.NewController())
|
fetcher.Setup(controller.NewController())
|
||||||
return fetcher.(*Fetcher)
|
return fetcher.(*Fetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildConfigFetcher(cfg config) fetcher.Fetcher {
|
||||||
|
fetcher := new(FetcherBuilder).Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
newController.GetConfig = func(v any) bool {
|
||||||
|
if err := json.Unmarshal([]byte(test.ToJson(cfg)), v); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
|
return fetcher
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import "github.com/monkeyWie/gopeed/pkg/base"
|
|
||||||
|
|
||||||
type chunk struct {
|
|
||||||
Status base.Status
|
|
||||||
Begin int64
|
|
||||||
End int64
|
|
||||||
Downloaded int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newChunk(begin int64, end int64) *chunk {
|
|
||||||
return &chunk{
|
|
||||||
Status: base.DownloadStatusReady,
|
|
||||||
Begin: begin,
|
|
||||||
End: end,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type extra struct {
|
|
||||||
Method string
|
|
||||||
Header map[string]string
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
+187
-19
@@ -1,13 +1,20 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/armon/go-socks5"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,8 +24,10 @@ const (
|
|||||||
Dir = "./"
|
Dir = "./"
|
||||||
BuildFile = Dir + BuildName
|
BuildFile = Dir + BuildName
|
||||||
|
|
||||||
DownloadName = "download.data"
|
DownloadName = "download.data"
|
||||||
DownloadFile = Dir + DownloadName
|
DownloadRename = "download (1).data"
|
||||||
|
DownloadFile = Dir + DownloadName
|
||||||
|
DownloadRenameFile = Dir + DownloadRename
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartTestFileServer() net.Listener {
|
func StartTestFileServer() net.Listener {
|
||||||
@@ -46,7 +55,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartTestChunkedServer() net.Listener {
|
func StartTestCustomServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func() 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) {
|
||||||
@@ -57,6 +66,17 @@ func StartTestChunkedServer() net.Listener {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
io.Copy(writer, file)
|
io.Copy(writer, file)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/disposition", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Disposition", "attachment; filename=\""+BuildName+"\"")
|
||||||
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -67,7 +87,7 @@ func StartTestRetryServer() net.Listener {
|
|||||||
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++
|
||||||
if counter != 1 && counter < 5 {
|
if counter != 1 && counter < 2 {
|
||||||
writer.WriteHeader(500)
|
writer.WriteHeader(500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -120,6 +140,123 @@ func StartTestErrorServer() net.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartTestLimitServer connections limit server
|
||||||
|
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||||
|
var connections atomic.Int32
|
||||||
|
|
||||||
|
return startTestServer(func() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
connections.Add(-1)
|
||||||
|
}()
|
||||||
|
connections.Add(1)
|
||||||
|
if maxConnections != 0 && connections.Load() > maxConnections {
|
||||||
|
writer.WriteHeader(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r := request.Header.Get("Range")
|
||||||
|
if r == "" {
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
writer.WriteHeader(200)
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
slowCopy(writer, file, delay)
|
||||||
|
} else {
|
||||||
|
// split range
|
||||||
|
s := strings.Split(r, "=")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = strings.Split(s[1], "-")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseInt(s[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseInt(s[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if start < 0 || end < 0 || start > end {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end >= BuildSize {
|
||||||
|
end = BuildSize - 1
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
||||||
|
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||||
|
writer.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
writer.WriteHeader(206)
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
file.Seek(start, 0)
|
||||||
|
slowCopyN(writer, file, end-start+1, delay)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return mux
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
buf := make([]byte, 32*1024)
|
||||||
|
for {
|
||||||
|
nr, er := src.Read(buf)
|
||||||
|
if nr > 0 {
|
||||||
|
nw, ew := dst.Write(buf[0:nr])
|
||||||
|
if nw > 0 {
|
||||||
|
written += int64(nw)
|
||||||
|
}
|
||||||
|
if ew != nil {
|
||||||
|
err = ew
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if nr != nw {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if er != nil {
|
||||||
|
if er != io.EOF {
|
||||||
|
err = er
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if delay > 0 {
|
||||||
|
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return written, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||||
|
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
|
||||||
|
if written == n {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
if written < n && err == nil {
|
||||||
|
// src stopped early; must have been EOF.
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
func startTestServer(serverHandle func() 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 {
|
||||||
@@ -129,6 +266,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
// 随机生成一个文件
|
// 随机生成一个文件
|
||||||
l := int64(8192)
|
l := int64(8192)
|
||||||
buf := make([]byte, l)
|
buf := make([]byte, l)
|
||||||
@@ -149,28 +287,29 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
server.Handler = serverHandle()
|
server.Handler = serverHandle()
|
||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
|
|
||||||
return &delFileListener{
|
return &shutdownListener{
|
||||||
File: file,
|
server: server,
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type delFileListener struct {
|
type shutdownListener struct {
|
||||||
*os.File
|
server *http.Server
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *delFileListener) Close() error {
|
func (c *shutdownListener) Close() error {
|
||||||
defer func() {
|
closeErr := c.server.Shutdown(context.Background())
|
||||||
c.File.Close()
|
if err := ifExistAndRemove(BuildFile); err != nil {
|
||||||
if err := ifExistAndRemove(c.File.Name()); err != nil {
|
fmt.Println(err)
|
||||||
fmt.Println(err)
|
}
|
||||||
}
|
if err := ifExistAndRemove(DownloadFile); err != nil {
|
||||||
if err := ifExistAndRemove(DownloadFile); err != nil {
|
fmt.Println(err)
|
||||||
fmt.Println(err)
|
}
|
||||||
}
|
if err := ifExistAndRemove(DownloadRenameFile); err != nil {
|
||||||
}()
|
fmt.Println(err)
|
||||||
return c.Listener.Close()
|
}
|
||||||
|
return closeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func ifExistAndRemove(name string) error {
|
func ifExistAndRemove(name string) error {
|
||||||
@@ -179,3 +318,32 @@ func ifExistAndRemove(name string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StartSocks5Server(usr, pwd string) net.Listener {
|
||||||
|
conf := &socks5.Config{}
|
||||||
|
if usr != "" && pwd != "" {
|
||||||
|
conf.Credentials = socks5.StaticCredentials{
|
||||||
|
usr: pwd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := socks5.New(conf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0") // 你可以根据需要更改监听地址
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
go server.Serve(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)
|
||||||
|
}
|
||||||
@@ -49,3 +49,7 @@ func ToJson(v interface{}) string {
|
|||||||
buf, _ := json.Marshal(v)
|
buf, _ := json.Marshal(v)
|
||||||
return string(buf)
|
return string(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func JsonEqual(v1 any, v2 any) bool {
|
||||||
|
return ToJson(v1) == ToJson(v2)
|
||||||
|
}
|
||||||
@@ -3,9 +3,10 @@ package base
|
|||||||
type Status string
|
type Status string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DownloadStatusReady Status = "ready"
|
DownloadStatusReady Status = "ready" // task create but not start
|
||||||
DownloadStatusRunning Status = "running"
|
DownloadStatusRunning Status = "running"
|
||||||
DownloadStatusPause Status = "pause"
|
DownloadStatusPause Status = "pause"
|
||||||
|
DownloadStatusWait Status = "wait" // task is wait for running
|
||||||
DownloadStatusError Status = "error"
|
DownloadStatusError Status = "error"
|
||||||
DownloadStatusDone Status = "done"
|
DownloadStatusDone Status = "done"
|
||||||
)
|
)
|
||||||
@@ -15,9 +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 = "User-Agent"
|
||||||
|
HttpHeaderLastModified = "Last-Modified"
|
||||||
|
|
||||||
|
HttpHeaderBytes = "bytes"
|
||||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
NotFound = errors.New("not found")
|
||||||
|
BadParams = errors.New("bad params")
|
||||||
|
)
|
||||||
+111
-23
@@ -1,42 +1,130 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
// 下载请求
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request download request
|
||||||
type Request struct {
|
type Request struct {
|
||||||
// 下载链接
|
URL string `json:"url"`
|
||||||
URL string `json:"url"`
|
Extra any `json:"extra"`
|
||||||
// 附加信息
|
// Labels is used to mark the download task
|
||||||
Extra interface{} `json:"extra"`
|
Labels map[string]string `json:"labels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 资源信息
|
func (r *Request) Validate() error {
|
||||||
|
if r.URL == "" {
|
||||||
|
return fmt.Errorf("invalid request url")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource download resource
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
Req *Request `json:"req"`
|
// if name is not empty, the resource is a folder and the name is the folder name
|
||||||
// 资源名称
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// 资源总大小
|
Size int64 `json:"size"`
|
||||||
Size int64 `json:"size"`
|
// is support range download
|
||||||
// 是否支持断点下载
|
|
||||||
Range bool `json:"range"`
|
Range bool `json:"range"`
|
||||||
// 资源所包含的文件列表
|
// file list
|
||||||
Files []*FileInfo `json:"files"`
|
Files []*FileInfo `json:"files"`
|
||||||
// 附加信息
|
Hash string `json:"hash"`
|
||||||
Extra interface{} `json:"extra"`
|
}
|
||||||
|
|
||||||
|
func (r *Resource) Validate() error {
|
||||||
|
if r.Name == "" {
|
||||||
|
return fmt.Errorf("invalid resource name")
|
||||||
|
}
|
||||||
|
if r.Files == nil || len(r.Files) == 0 {
|
||||||
|
return fmt.Errorf("invalid resource files")
|
||||||
|
}
|
||||||
|
for _, file := range r.Files {
|
||||||
|
if file.Name == "" {
|
||||||
|
return fmt.Errorf("invalid resource file name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) CalcSize(selectFiles []int) {
|
||||||
|
var size int64
|
||||||
|
for i, file := range r.Files {
|
||||||
|
if len(selectFiles) == 0 || slices.Contains(selectFiles, i) {
|
||||||
|
size += file.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.Size = size
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载选项
|
// Options for download
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// 保存文件名
|
// Download file name
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// 保存目录
|
// Download file path
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
// 并发连接数
|
// Select file indexes to download
|
||||||
Connections int `json:"connections"`
|
|
||||||
// 选择下载的文件下标列表
|
|
||||||
SelectFiles []int `json:"selectFiles"`
|
SelectFiles []int `json:"selectFiles"`
|
||||||
|
// Extra info for specific fetcher
|
||||||
|
Extra any `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) InitSelectFiles(fileSize int) {
|
||||||
|
// if selectFiles is empty, select all files
|
||||||
|
if len(o.SelectFiles) == 0 {
|
||||||
|
o.SelectFiles = make([]int, fileSize)
|
||||||
|
for i := 0; i < fileSize; i++ {
|
||||||
|
o.SelectFiles[i] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) Clone() *Options {
|
||||||
|
return &Options{
|
||||||
|
Name: o.Name,
|
||||||
|
Path: o.Path,
|
||||||
|
SelectFiles: o.SelectFiles,
|
||||||
|
Extra: o.Extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseReqExtra[E any](req *Request) error {
|
||||||
|
if req.Extra == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, ok := req.Extra.(*E); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var t E
|
||||||
|
if err := util.MapToStruct(req.Extra, &t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Extra = &t
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseOptsExtra[E any](opts *Options) error {
|
||||||
|
if opts.Extra == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, ok := opts.Extra.(*E); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var t E
|
||||||
|
if err := util.MapToStruct(opts.Extra, &t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts.Extra = &t
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
Loaded 50 of 309 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user