mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Compare commits
215
Commits
feature/bt
..
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
11015c7be1 | ||
|
|
57befd2c03 | ||
|
|
35156ed968 | ||
|
|
ee6c36174f | ||
|
|
fdfd95555d | ||
|
|
7ec824edda | ||
|
|
1f8b1be669 | ||
|
|
b0bcfeae34 | ||
|
|
10b346a326 | ||
|
|
a85af39b35 | ||
|
|
576f0d2e24 | ||
|
|
95eb3a427c | ||
|
|
e9442edd94 | ||
|
|
77e59f7376 | ||
|
|
135fba1b54 | ||
|
|
ff813e3bc9 | ||
|
|
babfe79023 | ||
|
|
89ac52bebd | ||
|
|
15e1c2f2d5 | ||
|
|
203f0ccc82 | ||
|
|
2422bd2c66 | ||
|
|
0306a1ed6b | ||
|
|
8a94650370 | ||
|
|
837a193e96 | ||
|
|
fcf0746f9a | ||
|
|
9eb466ccd3 | ||
|
|
c54f22613e | ||
|
|
3e1cfa0a27 | ||
|
|
9d22b49e41 | ||
|
|
1c6df55975 | ||
|
|
237616ded2 | ||
|
|
cebdda564e | ||
|
|
08ffb51946 | ||
|
|
8378f51f8e | ||
|
|
978d786f99 | ||
|
|
8b997fa7f2 | ||
|
|
745afb600c | ||
|
|
b45b840afb | ||
|
|
2a7a136203 | ||
|
|
dfb8958999 | ||
|
|
1568f4d6ff | ||
|
|
12ebceecb0 | ||
|
|
8cfe46cb71 | ||
|
|
2f1b929bcd | ||
|
|
2144cd8c3a | ||
|
|
080b34d070 | ||
|
|
de73d2a29f | ||
|
|
9de2b765fb | ||
|
|
be75b50521 | ||
|
|
39aec19471 | ||
|
|
9884001d54 | ||
|
|
29c834a070 | ||
|
|
b16f348676 | ||
|
|
7b46c2a2f6 | ||
|
|
1716d8b33e | ||
|
|
855f0b4002 | ||
|
|
ee0d081b83 | ||
|
|
d2f91a49ad | ||
|
|
fb6f889ddb | ||
|
|
0e6f1013cb | ||
|
|
fd10b18707 | ||
|
|
f423cf5b6d | ||
|
|
37b54b0c0f | ||
|
|
cd74c895d3 | ||
|
|
450027a8b0 | ||
|
|
48821a519e | ||
|
|
423ee5c3d1 | ||
|
|
96c8945ac8 | ||
|
|
b2b714ae88 | ||
|
|
2793ff696e | ||
|
|
2f81c5ef04 | ||
|
|
2eed470d5c | ||
|
|
34f8df1999 | ||
|
|
e901e08534 | ||
|
|
56561ba764 | ||
|
|
cf60e5a60a | ||
|
|
bf8e24a6af | ||
|
|
ff4e6f448c | ||
|
|
dccf080fbb | ||
|
|
9d7094133d | ||
|
|
ea6277805c | ||
|
|
3ad3060d71 | ||
|
|
c1545adeb2 | ||
|
|
9659176752 | ||
|
|
4371345c7d | ||
|
|
655f9d99c7 | ||
|
|
a9c6705bd3 | ||
|
|
46e0c6258f | ||
|
|
7c596c6b91 | ||
|
|
5e92379aa2 | ||
|
|
8a47b6cfb5 | ||
|
|
97eca4ec9c | ||
|
|
b0a7a5569a | ||
|
|
5073ae6166 | ||
|
|
81c6fcf15d | ||
|
|
2c6f821c4b | ||
|
|
236423fe3c | ||
|
|
a8136afc31 | ||
|
|
4d4d3b446a | ||
|
|
c4a30dc4c2 | ||
|
|
0fc133e46d | ||
|
|
c6aa83194f | ||
|
|
e292926697 | ||
|
|
8100bafdf9 | ||
|
|
e0582b5442 | ||
|
|
034137322f | ||
|
|
43378c7dc8 | ||
|
|
30b7189d59 | ||
|
|
bbd553d910 | ||
|
|
3996574b95 | ||
|
|
bd585309bf | ||
|
|
4eda5a3e76 | ||
|
|
d3c5dafd3e | ||
|
|
936e193c20 | ||
|
|
01501ca857 | ||
|
|
4763ff129f |
No files matched your search
@@ -0,0 +1,27 @@
|
|||||||
|
## Donate
|
||||||
|
|
||||||
|
Thank you for your support! 🙏
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>微信</th>
|
||||||
|
<th>支付宝</th>
|
||||||
|
<th>PayPal</th>
|
||||||
|
<th>Ko-fi</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="/.donate/qr/wechatpay.jpg" alt="wechatpay" width="300" /></td>
|
||||||
|
<td><img src="/.donate/qr/alipay.jpg" alt="alipay" width="300" /></td>
|
||||||
|
<td><a href="https://paypal.me/monkeyWie" target="_blank">Click Me</a></td>
|
||||||
|
<td><a href="https://ko-fi.com/gopeed" target="_blank">Click Me</a></td>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Crypto
|
||||||
|
|
||||||
|
- **BTC**: 35jYkPN8bgkh7AWjt3B1VgMQHqtGYcvF6p
|
||||||
|
- **ETH:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
||||||
|
- **Matic:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
||||||
|
- **BNB:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
||||||
|
- **USDT(ERC20)**: 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -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://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<!--
|
||||||
|
Please use the following template to create an issue.
|
||||||
|
-->
|
||||||
|
### Description(required)
|
||||||
|
### App Version(required)
|
||||||
|
### OS Version(required)
|
||||||
|
### Snapshots
|
||||||
|
### Log
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name-template: 'v$NEXT_PATCH_VERSION 🌈'
|
||||||
|
tag-template: 'v$NEXT_PATCH_VERSION'
|
||||||
|
categories:
|
||||||
|
- title: '🆕 Features'
|
||||||
|
labels:
|
||||||
|
- 'feat'
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
label: 'chore'
|
||||||
|
- title: '📖 Document'
|
||||||
|
label: 'doc'
|
||||||
|
- title: '🚀 CI/CD'
|
||||||
|
label: 'ci'
|
||||||
|
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||||
|
version-resolver:
|
||||||
|
major:
|
||||||
|
labels:
|
||||||
|
- 'major'
|
||||||
|
minor:
|
||||||
|
labels:
|
||||||
|
- 'minor'
|
||||||
|
patch:
|
||||||
|
labels:
|
||||||
|
- 'patch'
|
||||||
|
default: patch
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
+359
-37
@@ -1,48 +1,370 @@
|
|||||||
name: build
|
name: build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_dispatch:
|
||||||
push:
|
|
||||||
branches:
|
env:
|
||||||
- dev
|
GO_VERSION: 1.19
|
||||||
|
FLUTTER_VERSION: 3.7.12
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
get-release:
|
||||||
# name: Lint
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/setup-go@v2
|
|
||||||
# with:
|
|
||||||
# go-version: '^1.15.2'
|
|
||||||
# - uses: actions/checkout@v2
|
|
||||||
# - name: Lint Go Code
|
|
||||||
# run: |
|
|
||||||
# export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
|
|
||||||
# go get -u golang.org/x/lint/golint
|
|
||||||
# golint -set_exit_status ./...
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tag_name: ${{ steps.get-release.outputs.tag_name }}
|
||||||
|
upload_url: ${{ steps.get-release.outputs.upload_url }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: monkeyWie/get-latest-release@v1
|
||||||
|
id: get-release
|
||||||
with:
|
with:
|
||||||
go-version: '^1.15.2'
|
myToken: ${{ github.token }}
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Run Unit tests.
|
build-windows:
|
||||||
run: go test ./... -v -covermode=count -coverprofile=coverage.txt
|
runs-on: windows-latest
|
||||||
- uses: codecov/codecov-action@v1
|
needs: [get-release]
|
||||||
with:
|
|
||||||
file: ./coverage.txt
|
|
||||||
fail_ci_if_error: true
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# needs: [lint, test]
|
|
||||||
needs: [test]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '^1.15.2'
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: actions/checkout@v2
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -ldflags="-s -w" -o bin/gopeed cmd/gopeed/gopeed.go
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
|
run: |
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
|
||||||
|
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
|
||||||
|
|
||||||
|
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
|
||||||
|
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
|
||||||
|
$system = "C:\Windows\System32"
|
||||||
|
$release = "build\windows\runner\Release\"
|
||||||
|
cp $mingw\libstdc++-6.dll $release
|
||||||
|
cp $mingw\libgcc_s_seh-1.dll $release
|
||||||
|
cp $mingw\libwinpthread-1.dll $release
|
||||||
|
cp $system\msvcp140.dll $release
|
||||||
|
cp $system\vcruntime140.dll $release
|
||||||
|
cp $system\vcruntime140_1.dll $release
|
||||||
|
|
||||||
|
New-Item -Path build\windows\Output -ItemType Directory
|
||||||
|
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$env:VERSION-windows-amd64-portable.zip"
|
||||||
|
|
||||||
|
cd build/windows
|
||||||
|
echo @"
|
||||||
|
; Script generated by the Inno Setup Script Wizard.
|
||||||
|
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||||
|
|
||||||
|
#define MyAppName "Gopeed"
|
||||||
|
#define MyAppVersion "$($env:VERSION.substring(1))"
|
||||||
|
#define MyAppPublisher "monkeyWie"
|
||||||
|
#define MyAppURL "https://gopeed.com"
|
||||||
|
#define MyAppExeName "gopeed.exe"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||||
|
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||||
|
AppId={{5960F34D-1E42-402C-8C85-DE2FF24CBAE4}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
AppPublisherURL={#MyAppURL}
|
||||||
|
AppSupportURL={#MyAppURL}
|
||||||
|
AppUpdatesURL={#MyAppURL}
|
||||||
|
DefaultDirName={autopf}\gopeed
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
LicenseFile=..\..\..\..\LICENSE
|
||||||
|
; Remove the following line to run in administrative install mode (install for all users.)
|
||||||
|
PrivilegesRequired=lowest
|
||||||
|
OutputBaseFilename=gopeed
|
||||||
|
SetupIconFile=..\..\assets\icon\icon.ico
|
||||||
|
Compression=lzma
|
||||||
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
LanguageDetectionMethod=uilanguage
|
||||||
|
ShowLanguageDialog=yes
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
|
||||||
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{app}"
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||||
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
|
"@ > setup.iss
|
||||||
|
iscc.exe setup.iss
|
||||||
|
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
||||||
|
Compress-Archive -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe" -DestinationPath "Output\Gopeed-$env:VERSION-windows-amd64.zip"
|
||||||
|
Remove-Item -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe"
|
||||||
|
- name: Upload
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
|
asset_path: ui/flutter/build/windows/Output/*
|
||||||
|
overwrite: true
|
||||||
|
build-macos-arm64-lib:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [get-release]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
docker pull techknowlogick/xgo:go-1.19.x
|
||||||
|
go install src.techknowlogick.com/xgo@latest
|
||||||
|
xgo --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
||||||
|
mv libgopeed-*.dylib libgopeed.dylib
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: macos-arm64-lib
|
||||||
|
path: libgopeed.dylib
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs: [get-release, build-macos-arm64-lib]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Install appdmg
|
||||||
|
run: npm install -g appdmg
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: macos-arm64-lib
|
||||||
|
path: ui/flutter/lib-arm64
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
|
run: |
|
||||||
|
PROJECT_DIR=$(pwd)
|
||||||
|
|
||||||
|
# amd64 lib
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
# universal binary
|
||||||
|
mkdir -p ui/flutter/macos/Frameworks
|
||||||
|
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
|
||||||
|
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
|
||||||
|
cd ui/flutter/macos/Frameworks
|
||||||
|
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
|
||||||
|
|
||||||
|
cd $PROJECT_DIR/ui/flutter
|
||||||
|
flutter build macos
|
||||||
|
cd build/macos/Build/Products/Release
|
||||||
|
cat>appdmg.json<<EOF
|
||||||
|
{
|
||||||
|
"title": "Gopeed",
|
||||||
|
"icon": "Gopeed.app/Contents/Resources/AppIcon.icns",
|
||||||
|
"contents": [
|
||||||
|
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
|
||||||
|
{ "x": 192, "y": 344, "type": "file", "path": "Gopeed.app" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
mkdir dist
|
||||||
|
appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
|
||||||
|
- name: Upload
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
|
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
|
||||||
|
overwrite: true
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [get-release]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
|
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
|
||||||
|
mkdir -p debian/gui
|
||||||
|
cp assets/icon/icon_1024.png debian/gui/gopeed.png
|
||||||
|
cat>debian/debian.yaml<<EOF
|
||||||
|
flutter_app:
|
||||||
|
command: gopeed
|
||||||
|
arch: x64
|
||||||
|
parent: /usr/local/lib
|
||||||
|
control:
|
||||||
|
Package: gopeed
|
||||||
|
Version: ${VERSION:1}
|
||||||
|
Architecture: amd64
|
||||||
|
Essential: no
|
||||||
|
Priority: optional
|
||||||
|
Depends:
|
||||||
|
Maintainer: monkeyWie
|
||||||
|
Description: High speed downloader that supports all platforms.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat>debian/gui/gopeed.desktop<<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Version=${VERSION:1}
|
||||||
|
Name=Gopeed
|
||||||
|
GenericName=Gopeed
|
||||||
|
Comment=High speed downloader that supports all platforms.
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility
|
||||||
|
Keywords=Flutter;Downloader;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
dart pub global activate flutter_to_debian
|
||||||
|
flutter_to_debian
|
||||||
|
mv debian/packages/gopeed*.deb debian/packages/Gopeed-$VERSION-linux-amd64.deb
|
||||||
|
- name: Upload
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
|
asset_path: ui/flutter/debian/packages/*
|
||||||
|
overwrite: true
|
||||||
|
build-android:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [get-release]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: "zulu"
|
||||||
|
java-version: "11"
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
|
APK_KEYSTORE : ${{ secrets.APK_KEYSTORE }}
|
||||||
|
APK_KEY_PASSWORD : ${{ secrets.APK_KEY_PASSWORD }}
|
||||||
|
APK_STORE_PASSWORD : ${{ secrets.APK_STORE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
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
|
||||||
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
|
flutter build apk
|
||||||
|
mkdir dist
|
||||||
|
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk
|
||||||
|
- name: Upload
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
|
asset_path: ui/flutter/dist/*
|
||||||
|
overwrite: true
|
||||||
|
build-web:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [get-release]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
|
run: |
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web
|
||||||
|
cd ../../
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
mkdir -p dist/zip
|
||||||
|
goos_arr=(windows darwin linux)
|
||||||
|
goarch_arr=(386 amd64 arm64)
|
||||||
|
export CGO_ENABLED=0
|
||||||
|
for goos in "${goos_arr[@]}"; do
|
||||||
|
for goarch in "${goarch_arr[@]}"; do
|
||||||
|
goos_name=$goos
|
||||||
|
if [ $goos = "darwin" ]; then
|
||||||
|
goos_name="macos"
|
||||||
|
fi
|
||||||
|
name=gopeed-web-$VERSION-$goos_name-$goarch
|
||||||
|
dir="dist/$name/"
|
||||||
|
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w" -o $dir github.com/GopeedLab/gopeed/cmd/web \
|
||||||
|
&& cd $dir \
|
||||||
|
&& file=$(ls -AU | head -1) \
|
||||||
|
&& mkdir $name \
|
||||||
|
&& mv $file $name/$(echo $file | sed -e "s/web/gopeed/g") \
|
||||||
|
&& zip -r ../zip/$name.zip * \
|
||||||
|
&& cd ../..) \
|
||||||
|
|| true
|
||||||
|
done
|
||||||
|
done
|
||||||
|
- name: Upload
|
||||||
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
|
with:
|
||||||
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
|
asset_path: dist/zip/*
|
||||||
|
overwrite: true
|
||||||
|
build-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [get-release]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: liwei2633
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
platforms: |
|
||||||
|
linux/386
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
|
tags: |
|
||||||
|
liwei2633/gopeed:latest
|
||||||
|
liwei2633/gopeed:${{ env.VERSION }}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "bind/**"
|
||||||
|
- "cmd/**"
|
||||||
|
- "internal/**"
|
||||||
|
- "pkg/**"
|
||||||
|
- "ui/**"
|
||||||
|
- ".github/workflows/release.yml"
|
||||||
|
- ".github/release-drafter.yml"
|
||||||
|
- "go.mod"
|
||||||
|
- "go.sum"
|
||||||
|
- "Dockerfile"
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: 1.19
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tag_name: ${{ steps.create_release.outputs.tag_name }}
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
id: create_release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "bind/**"
|
||||||
|
- "cmd/**"
|
||||||
|
- "internal/**"
|
||||||
|
- "pkg/**"
|
||||||
|
- "ui/**"
|
||||||
|
- ".github/workflows/test.yml"
|
||||||
|
- "go.mod"
|
||||||
|
- "go.sum"
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- "bind/**"
|
||||||
|
- "cmd/**"
|
||||||
|
- "internal/**"
|
||||||
|
- "pkg/**"
|
||||||
|
- "ui/**"
|
||||||
|
- ".github/workflows/test.yml"
|
||||||
|
- "go.mod"
|
||||||
|
- "go.sum"
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: 1.19
|
||||||
|
FLUTTER_VERSION: 3.7.12
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# lint:
|
||||||
|
# name: Lint
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/setup-go@v2
|
||||||
|
# with:
|
||||||
|
# go-version: '^1.19'
|
||||||
|
# - uses: actions/checkout@v2
|
||||||
|
# - name: Lint Go Code
|
||||||
|
# run: |
|
||||||
|
# export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
|
||||||
|
# go get -u golang.org/x/lint/golint
|
||||||
|
# golint -set_exit_status ./...
|
||||||
|
test:
|
||||||
|
name: Test check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- name: Run Unit tests.
|
||||||
|
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
|
||||||
|
- uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
files: ./coverage.txt
|
||||||
|
build:
|
||||||
|
name: Build check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# needs: [lint, test]
|
||||||
|
needs: [test]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
- name: Build
|
||||||
|
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
|
||||||
+13
-2
@@ -11,6 +11,17 @@
|
|||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
debug
|
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
bin/
|
||||||
|
|
||||||
|
.torrent.db
|
||||||
|
.torrent.db-shm
|
||||||
|
.torrent.db-wal
|
||||||
|
|
||||||
|
*.data
|
||||||
|
*.db
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# 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/assets/locales` directory.
|
||||||
|
You only need to add the corresponding language file in this directory.
|
||||||
|
|
||||||
|
|
||||||
|
Generate locales after you edit locales:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
get generate locales
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
get-cli commands usages:
|
||||||
|
|
||||||
|
```
|
||||||
|
create:
|
||||||
|
controller: Generate controller
|
||||||
|
page: Use to generate pages
|
||||||
|
view: Generate view
|
||||||
|
generate:
|
||||||
|
locales: Generate translation file from json files
|
||||||
|
model: generate Class model from json
|
||||||
|
```
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Gopeed 贡献指南
|
||||||
|
|
||||||
|
首先感谢您对贡献代码感兴趣,这份指南将帮助您更好的参与到 Gopeed 的开发中来。
|
||||||
|
|
||||||
|
## 分支说明
|
||||||
|
|
||||||
|
本项目只有一个主分支,即 `main` 分支,如果您想要参与到 Gopeed 的开发中来,请先 fork 本项目,然后在您的 fork 项目中进行开发,开发完成后再向本项目提交
|
||||||
|
PR,合并到 `main` 分支。
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
建议通过 web 端进行开发调试,首先启动后端服务,通过命令行 `go run cmd/api/main.go` 启动 ,服务启动默认端口为 `9999`,然后以 `debug` 模式启动前端
|
||||||
|
flutter 项目即可运行。
|
||||||
|
|
||||||
|
## 翻译
|
||||||
|
|
||||||
|
Gopeed 的国际化文件位于 `ui/flutter/assets/locales` 目录下,只需要在该目录下添加对应的语言文件即可。
|
||||||
|
|
||||||
|
编辑locales后请运行以下命令:
|
||||||
|
|
||||||
|
```
|
||||||
|
get generate locales
|
||||||
|
```
|
||||||
|
|
||||||
|
请注意以 `en_US.json` 为参照进行翻译。
|
||||||
|
|
||||||
|
## flutter开发
|
||||||
|
|
||||||
|
每次提交前请务必`dart format ./ui/flutter`
|
||||||
|
|
||||||
|
如果要编辑api/models,请打开build_runner watcher:
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter pub run build_runner watch
|
||||||
|
```
|
||||||
|
|
||||||
|
适用get-cli 命令:
|
||||||
|
|
||||||
|
```
|
||||||
|
create:
|
||||||
|
controller: Generate controller
|
||||||
|
page: Use to generate pages
|
||||||
|
view: Generate view
|
||||||
|
generate:
|
||||||
|
locales: Generate translation file from json files
|
||||||
|
model: generate Class model from json
|
||||||
|
```
|
||||||
|
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
FROM cirrusci/flutter:3.7.7 AS flutter
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
||||||
|
RUN flutter pub get
|
||||||
|
COPY ./ui/flutter ./
|
||||||
|
RUN flutter build web
|
||||||
|
|
||||||
|
FROM golang:1.19.3 AS go
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ./go.mod ./go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
COPY --from=flutter /app/build/web ./cmd/web/dist
|
||||||
|
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
|
||||||
|
FROM alpine:3.14.2
|
||||||
|
LABEL maintainer="monkeyWie"
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=go /app/dist/gopeed ./
|
||||||
|
EXPOSE 9999
|
||||||
|
ENTRYPOINT ["./gopeed"]
|
||||||
@@ -1,38 +1,170 @@
|
|||||||
[](https://github.com/monkeyWie/gopeed-core/actions?query=workflow%3Abuild)
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
[](https://codecov.io/gh/monkeyWie/gopeed-core)
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
## gopeed
|

|
||||||
|
|
||||||
支持多协议(HTTP、BitTorrent)下载的客户端,提供命令行、RESTful API、WebSocket、Go 类库方式来使用。
|
[English](/README.md) | [中文](/README_zh-CN.md)
|
||||||
|
|
||||||
## 安装
|
## Introduction
|
||||||
|
|
||||||
```sh
|
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms.
|
||||||
go install github.com/monkeyWie/gopeed-core
|
|
||||||
|
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
**Supported platforms**
|
||||||
|
|
||||||
|
- [x] windows
|
||||||
|
- [x] macos
|
||||||
|
- [x] linux
|
||||||
|
- [x] android
|
||||||
|
- [ ] ios
|
||||||
|
- [x] web
|
||||||
|
- [x] docker
|
||||||
|
|
||||||
|
[Download](https://github.com/GopeedLab/gopeed/releases/latest)
|
||||||
|
|
||||||
|
> Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal
|
||||||
|
|
||||||
|
### Command tool
|
||||||
|
|
||||||
|
use `go install`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## 示例
|
### Docker
|
||||||
|
|
||||||
```go
|
#### Start directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO
|
#### Using Docker Compose
|
||||||
|
|
||||||
- [x] HTTP 下载实现
|
```bash
|
||||||
- [ ] BitTorrent 下载实现
|
docker-compose up -d
|
||||||
- [x] .torrent 文件解析
|
```
|
||||||
- [x] tracker 协议实现
|
|
||||||
- [x] peer wire protocol 协议实现
|
|
||||||
- [ ] DHT 协议实现
|
|
||||||
- [ ] 磁力链接支持
|
|
||||||
- [ ] uTP 协议实现
|
|
||||||
- [x] 下载接口抽象(不关心具体协议)
|
|
||||||
- [ ] 支持自定义配置
|
|
||||||
- [ ] 限速功能实现
|
|
||||||
- [x] 命令行工具提供
|
|
||||||
- [ ] RESTful 服务提供
|
|
||||||
|
|
||||||
## 参与
|
#### Access Gopeed
|
||||||
|
|
||||||
由于项目目前还未定型,代码可能随时有大的调整,所以暂不接受 PR,当然如果有什么好的想法可以在 issue 区提出来。
|
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.
|
||||||
|
|
||||||
|
## Donate
|
||||||
|
|
||||||
|
If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
|
||||||
|
|
||||||
|
## Showcase
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
|
||||||
|
|
||||||
|
> The front code is located in the `ui/flutter` directory.
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
1. Golang 1.19+
|
||||||
|
2. Flutter 3.7+
|
||||||
|
|
||||||
|
### Clone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:monkeyWie/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
#### 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.
|
||||||
|
|
||||||
|
command:
|
||||||
|
|
||||||
|
- windows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build windows
|
||||||
|
```
|
||||||
|
|
||||||
|
- macos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build macos
|
||||||
|
```
|
||||||
|
|
||||||
|
- linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build linux
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mobile
|
||||||
|
|
||||||
|
Same as before, you also need to prepare the `cgo` environment, and then install `gomobile`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
```
|
||||||
|
|
||||||
|
command:
|
||||||
|
|
||||||
|
- android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build apk
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web
|
||||||
|
|
||||||
|
command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
go run cmd/web/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
[GPLv3](LICENSE)
|
||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
[](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/blob/main/.donate/index.md#donate)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md)
|
||||||
|
|
||||||
|
## 介绍
|
||||||
|
|
||||||
|
Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
|
||||||
|
|
||||||
|
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
**已支持平台**
|
||||||
|
|
||||||
|
- [x] windows
|
||||||
|
- [x] macos
|
||||||
|
- [x] linux
|
||||||
|
- [x] android
|
||||||
|
- [ ] ios
|
||||||
|
- [x] web
|
||||||
|
- [x] docker
|
||||||
|
|
||||||
|
[前往下载](https://github.com/GopeedLab/gopeed/releases/latest)
|
||||||
|
|
||||||
|
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
|
||||||
|
|
||||||
|
### 命令行工具
|
||||||
|
|
||||||
|
使用`go install`安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
#### 直接运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用 Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 访问服务
|
||||||
|
|
||||||
|
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
||||||
|
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
|
||||||
|
|
||||||
|
## 打赏
|
||||||
|
|
||||||
|
如果觉得项目对你有帮助,请考虑[打赏](/.donate/index.md#donate)以支持这个项目的发展,非常感谢!
|
||||||
|
|
||||||
|
## 界面展示
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
|
||||||
|
|
||||||
|
> 前端代码位于`ui/flutter`目录下。
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
1. Golang 1.19+
|
||||||
|
2. Flutter 3.7+
|
||||||
|
|
||||||
|
### 克隆项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:monkeyWie/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### 贡献代码
|
||||||
|
|
||||||
|
请参考[贡献指南](CONTRIBUTING_zh-CN.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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
go run cmd/web/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 感谢
|
||||||
|
|
||||||
|
### 贡献者
|
||||||
|
|
||||||
|
<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) 协议开源。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
finallyCh := make(chan error)
|
||||||
|
_, err := download.Boot().
|
||||||
|
URL("https://www.baidu.com/index.html").
|
||||||
|
Listener(func(event *download.Event) {
|
||||||
|
if event.Key == download.EventKeyFinally {
|
||||||
|
finallyCh <- event.Err
|
||||||
|
}
|
||||||
|
}).
|
||||||
|
Create(&base.Options{
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 8,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = <-finallyCh
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("download fail:%v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("download success")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
|
||||||
|
//export Start
|
||||||
|
func Start(cfg *C.char) (int, *C.char) {
|
||||||
|
var config model.StartConfig
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(cfg)), &config); err != nil {
|
||||||
|
return 0, C.CString(err.Error())
|
||||||
|
}
|
||||||
|
realPort, err := rest.Start(&config)
|
||||||
|
if err != nil {
|
||||||
|
return 0, C.CString(err.Error())
|
||||||
|
}
|
||||||
|
return realPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//export Stop
|
||||||
|
func Stop() {
|
||||||
|
rest.Stop()
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package libgopeed
|
||||||
|
|
||||||
|
// #cgo LDFLAGS: -static-libstdc++
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start(cfg string) (int, error) {
|
||||||
|
var config model.StartConfig
|
||||||
|
if err := json.Unmarshal([]byte(cfg), &config); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return rest.Start(&config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() {
|
||||||
|
rest.Stop()
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &model.StartConfig{
|
||||||
|
Network: "tcp",
|
||||||
|
Address: "127.0.0.1:9999",
|
||||||
|
Storage: model.StorageBolt,
|
||||||
|
}
|
||||||
|
cmd.Start(cfg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
_______ ______ .______ _______ _______ _______
|
||||||
|
/ _____| / __ \ | _ \ | ____|| ____|| \
|
||||||
|
| | __ | | | | | |_) | | |__ | |__ | .--. |
|
||||||
|
| | |_ | | | | | | ___/ | __| | __| | | | |
|
||||||
|
| |__| | | `--' | | | | |____ | |____ | '--' |
|
||||||
|
\______| \______/ | _| |_______||_______||_______/
|
||||||
+3
-4
@@ -4,7 +4,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
@@ -14,19 +13,19 @@ type args struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parse() *args {
|
func parse() *args {
|
||||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
var args args
|
var args args
|
||||||
args.connections = flag.Int("C", 16, "Concurrent connections.")
|
args.connections = flag.Int("C", 16, "Concurrent connections.")
|
||||||
args.dir = flag.String("D", dir, "Save directory.")
|
args.dir = flag.String("D", dir, "Store directory.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
t := flag.Args()
|
t := flag.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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/monkeyWie/gopeed-core/internal/protocol/http"
|
|
||||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
|
||||||
"github.com/monkeyWie/gopeed-core/pkg/download"
|
|
||||||
"github.com/monkeyWie/gopeed-core/pkg/util"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const progressWidth = 20
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
args := parse()
|
|
||||||
|
|
||||||
downloader := download.NewDownloader(http.FetcherBuilder)
|
|
||||||
res, err := downloader.Resolve(&base.Request{
|
|
||||||
URL: args.url,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = downloader.Create(res, &base.Options{
|
|
||||||
Path: *args.dir,
|
|
||||||
Connections: *args.connections,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
downloader.Listener(func(taskInfo *download.TaskInfo, eventKey download.EventKey) {
|
|
||||||
if eventKey == download.EventKeyProgress {
|
|
||||||
printProgress(taskInfo, "downloading...")
|
|
||||||
}
|
|
||||||
if eventKey == download.EventKeyDone {
|
|
||||||
printProgress(taskInfo, "complete")
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
wg.Wait()
|
|
||||||
fmt.Println()
|
|
||||||
gPrint("saving file " + filepath.Join(*args.dir, res.Files[0].Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func printProgress(taskInfo *download.TaskInfo, title string) {
|
|
||||||
rate := float64(taskInfo.Progress.Downloaded) / float64(taskInfo.Res.TotalSize)
|
|
||||||
completeWidth := int(progressWidth * rate)
|
|
||||||
speed := util.ByteFmt(taskInfo.Progress.Speed)
|
|
||||||
totalSize := util.ByteFmt(taskInfo.Res.TotalSize)
|
|
||||||
fmt.Printf("\r%s [", title)
|
|
||||||
for i := 0; i < progressWidth; i++ {
|
|
||||||
if i < completeWidth {
|
|
||||||
fmt.Print("■")
|
|
||||||
} else {
|
|
||||||
fmt.Print("□")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("] %.1f%% %s/s %s", rate*100, speed, totalSize)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const progressWidth = 20
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
//args := parse()
|
||||||
|
var dir = "./"
|
||||||
|
var connections = 8
|
||||||
|
args := &args{
|
||||||
|
url: "https://sel-kor-ping.vultr.com/vultr.com.100MB.bin",
|
||||||
|
dir: &dir,
|
||||||
|
connections: &connections,
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
_, err := download.Boot().
|
||||||
|
URL(args.url).
|
||||||
|
Listener(func(event *download.Event) {
|
||||||
|
if event.Key == download.EventKeyProgress {
|
||||||
|
printProgress(event.Task, "downloading...")
|
||||||
|
}
|
||||||
|
if event.Key == download.EventKeyFinally {
|
||||||
|
var title string
|
||||||
|
if event.Err != nil {
|
||||||
|
title = "fail"
|
||||||
|
} else {
|
||||||
|
title = "complete"
|
||||||
|
}
|
||||||
|
printProgress(event.Task, title)
|
||||||
|
fmt.Println()
|
||||||
|
if event.Err != nil {
|
||||||
|
fmt.Printf("reason: %s", event.Err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Printf("saving path: %s", *args.dir)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
}).
|
||||||
|
Create(&base.Options{
|
||||||
|
Path: *args.dir,
|
||||||
|
Extra: http.OptsExtra{Connections: *args.connections},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lastLineLen = 0
|
||||||
|
sb = new(strings.Builder)
|
||||||
|
)
|
||||||
|
|
||||||
|
func printProgress(task *download.Task, title string) {
|
||||||
|
rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
|
||||||
|
completeWidth := int(progressWidth * rate)
|
||||||
|
speed := util.ByteFmt(task.Progress.Speed)
|
||||||
|
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
||||||
|
sb.WriteString(fmt.Sprintf("\r%s [", title))
|
||||||
|
for i := 0; i < progressWidth; i++ {
|
||||||
|
if i < completeWidth {
|
||||||
|
sb.WriteString("■")
|
||||||
|
} else {
|
||||||
|
sb.WriteString("□")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("] %.1f%% %s/s %s", rate*100, speed, totalSize))
|
||||||
|
if lastLineLen != 0 {
|
||||||
|
paddingLen := lastLineLen - sb.Len()
|
||||||
|
if paddingLen > 0 {
|
||||||
|
sb.WriteString(strings.Repeat(" ", paddingLen))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastLineLen = sb.Len()
|
||||||
|
fmt.Print(sb.String())
|
||||||
|
sb.Reset()
|
||||||
|
}
|
||||||
@@ -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,29 @@
|
|||||||
|
//go:build web
|
||||||
|
// +build web
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed dist/*
|
||||||
|
var dist embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sub, err := fs.Sub(dist, "dist")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cfg := &model.StartConfig{
|
||||||
|
Network: "tcp",
|
||||||
|
Address: "0.0.0.0:9999",
|
||||||
|
Storage: model.StorageBolt,
|
||||||
|
WebEnable: true,
|
||||||
|
WebFS: sub,
|
||||||
|
}
|
||||||
|
cmd.Start(cfg)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gopeed:
|
||||||
|
container_name: gopeed
|
||||||
|
ports:
|
||||||
|
- 9999:9999 # HTTP port (host:container)
|
||||||
|
image: liwei2633/gopeed
|
||||||
|
volumes:
|
||||||
|
- ~/Downloads:/download # replace with your prefer path
|
||||||
|
restart: unless-stopped
|
||||||
@@ -1,17 +1,73 @@
|
|||||||
module github.com/monkeyWie/gopeed-core
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.13
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/RoaringBitmap/roaring v0.4.21
|
github.com/anacrolix/torrent v1.46.0
|
||||||
github.com/cenkalti/mse v0.0.0-20140930130441-6ef65f170972
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gizak/termui/v3 v3.1.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/google/uuid v1.1.1
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
github.com/marksamman/bencode v0.0.0-20150821143521-dc84f26e086e
|
go.etcd.io/bbolt v1.3.6
|
||||||
github.com/mattn/go-tty v0.0.3 // indirect
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||||
github.com/sirupsen/logrus v1.4.2
|
)
|
||||||
github.com/superhawk610/bar v0.0.2
|
|
||||||
github.com/superhawk610/terminal v0.1.0 // indirect
|
require (
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
github.com/RoaringBitmap/roaring v1.0.1-0.20220510143707-3f418c4f42a4 // indirect
|
||||||
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||||
|
github.com/anacrolix/chansync v0.3.0 // indirect
|
||||||
|
github.com/anacrolix/dht/v2 v2.16.2-0.20220311024416-dd658f18fd51 // indirect
|
||||||
|
github.com/anacrolix/envpprof v1.2.1 // indirect
|
||||||
|
github.com/anacrolix/generics v0.0.0-20220618083756-f99e35403a60 // indirect
|
||||||
|
github.com/anacrolix/go-libutp v1.2.0 // indirect
|
||||||
|
github.com/anacrolix/log v0.13.1 // indirect
|
||||||
|
github.com/anacrolix/missinggo v1.3.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/multiless v0.2.1-0.20211218050420-533661eef5dc // indirect
|
||||||
|
github.com/anacrolix/stm v0.3.0 // indirect
|
||||||
|
github.com/anacrolix/sync v0.4.0 // indirect
|
||||||
|
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
|
||||||
|
github.com/anacrolix/utp v0.1.0 // indirect
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
|
github.com/benbjohnson/immutable v0.3.0 // indirect
|
||||||
|
github.com/bits-and-blooms/bitset v1.2.0 // indirect
|
||||||
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||||
|
github.com/google/btree v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
|
github.com/lispad/go-generics-tools v1.0.0 // indirect
|
||||||
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
|
github.com/pion/datachannel v1.5.2 // indirect
|
||||||
|
github.com/pion/dtls/v2 v2.1.2 // indirect
|
||||||
|
github.com/pion/ice/v2 v2.1.20 // indirect
|
||||||
|
github.com/pion/interceptor v0.1.7 // indirect
|
||||||
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
|
github.com/pion/mdns v0.0.5 // indirect
|
||||||
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
|
github.com/pion/rtcp v1.2.9 // indirect
|
||||||
|
github.com/pion/rtp v1.7.4 // indirect
|
||||||
|
github.com/pion/sctp v1.8.2 // indirect
|
||||||
|
github.com/pion/sdp/v3 v3.0.4 // indirect
|
||||||
|
github.com/pion/srtp/v2 v2.0.5 // indirect
|
||||||
|
github.com/pion/stun v0.3.5 // indirect
|
||||||
|
github.com/pion/transport v0.13.0 // indirect
|
||||||
|
github.com/pion/turn/v2 v2.0.6 // indirect
|
||||||
|
github.com/pion/udp v0.1.1 // indirect
|
||||||
|
github.com/pion/webrtc/v3 v3.1.24-0.20220208053747-94262c1b2b38 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rs/dnscache v0.0.0-20210201191234-295bba877686 // indirect
|
||||||
|
github.com/tidwall/btree v1.3.1 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||||
|
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,61 +1,52 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"golang.org/x/net/proxy"
|
"errors"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller interface {
|
type Controller struct {
|
||||||
|
GetConfig func(v any) (bool, error)
|
||||||
|
FileController
|
||||||
|
//ContextDialer() (proxy.Dialer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileController interface {
|
||||||
Touch(name string, size int64) (file *os.File, err error)
|
Touch(name string, size int64) (file *os.File, err error)
|
||||||
Open(name string) (file *os.File, err error)
|
|
||||||
Write(name string, offset int64, buf []byte) (int, error)
|
|
||||||
Close(name string) error
|
|
||||||
ContextDialer() (proxy.Dialer, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultController struct {
|
type DefaultFileController struct {
|
||||||
Files map[string]*os.File
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController() *DefaultController {
|
func NewController() *Controller {
|
||||||
return &DefaultController{Files: make(map[string]*os.File)}
|
return &Controller{
|
||||||
|
FileController: &DefaultFileController{},
|
||||||
|
GetConfig: func(v any) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultController) Touch(name string, size int64) (file *os.File, err error) {
|
func (c *DefaultFileController) Touch(name string, size int64) (file *os.File, err error) {
|
||||||
|
dir := filepath.Dir(name)
|
||||||
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
file, err = os.Create(name)
|
file, err = os.Create(name)
|
||||||
|
if err != nil && !errors.Is(err, os.ErrExist) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if size > 0 {
|
if size > 0 {
|
||||||
err = os.Truncate(name, size)
|
err = os.Truncate(name, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
|
||||||
c.Files[name] = file
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultController) Open(name string) (file *os.File, err error) {
|
/*func (c *DefaultController) ContextDialer() (proxy.Dialer, error) {
|
||||||
file, err = os.OpenFile(name, os.O_RDWR, os.ModePerm)
|
|
||||||
if err == nil {
|
|
||||||
c.Files[name] = file
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DefaultController) Write(name string, offset int64, buf []byte) (int, error) {
|
|
||||||
return c.Files[name].WriteAt(buf, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DefaultController) Close(name string) error {
|
|
||||||
err := c.Files[name].Close()
|
|
||||||
delete(c.Files, name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DefaultController) ContextDialer() (proxy.Dialer, error) {
|
|
||||||
// return proxy.SOCKS5("tpc", "127.0.0.1:9999", nil, nil)
|
// return proxy.SOCKS5("tpc", "127.0.0.1:9999", nil, nil)
|
||||||
var dialer proxy.Dialer
|
var dialer proxy.Dialer
|
||||||
return &DialerWarp{dialer: dialer}, nil
|
return &DialerWarp{dialer: dialer}, nil
|
||||||
@@ -107,4 +98,4 @@ func (d *DialerWarp) Dial(network, addr string) (c net.Conn, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ConnWarp{conn: conn}, nil
|
return &ConnWarp{conn: conn}, nil
|
||||||
}
|
}*/
|
||||||
+56
-18
@@ -1,45 +1,83 @@
|
|||||||
package fetcher
|
package fetcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/monkeyWie/gopeed-core/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/monkeyWie/gopeed-core/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)
|
// 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)
|
|
||||||
|
|
||||||
// 获取任务各个文件下载进度
|
Setup(ctl *controller.Controller) error
|
||||||
|
// 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
|
||||||
|
Continue() error
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FetcherMeta) Filepath(file *base.FileInfo) string {
|
||||||
|
finalName := m.Opts.Name
|
||||||
|
if finalName == "" {
|
||||||
|
finalName = file.Name
|
||||||
|
}
|
||||||
|
return path.Join(m.Opts.Path, m.Res.RootDir, finalName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetcherBuilder defines the interface for a fetcher builder.
|
||||||
|
type FetcherBuilder interface {
|
||||||
|
// Schemes returns the schemes supported by the fetcher.
|
||||||
|
Schemes() []string
|
||||||
|
// Build returns a new fetcher.
|
||||||
|
Build() Fetcher
|
||||||
|
|
||||||
|
// Store fetcher
|
||||||
|
Store(fetcher Fetcher) (any, error)
|
||||||
|
// Restore 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) {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *DefaultFetcher) Wait() (err error) {
|
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,5 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Trackers []string `json:"trackers"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var client *torrent.Client
|
||||||
|
|
||||||
|
type Fetcher struct {
|
||||||
|
ctl *controller.Controller
|
||||||
|
|
||||||
|
torrent *torrent.Torrent
|
||||||
|
meta *fetcher.FetcherMeta
|
||||||
|
|
||||||
|
ready atomic.Bool
|
||||||
|
progress fetcher.Progress
|
||||||
|
|
||||||
|
torrentPaths map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Name() string {
|
||||||
|
return "bt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
||||||
|
f.ctl = ctl
|
||||||
|
if f.meta == nil {
|
||||||
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
|
}
|
||||||
|
var once sync.Once
|
||||||
|
once.Do(func() {
|
||||||
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.ListenPort = 0
|
||||||
|
|
||||||
|
// Support custom download path for each torrent
|
||||||
|
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
|
||||||
|
if err != nil {
|
||||||
|
pieceCompletion = storage.NewMapPieceCompletion()
|
||||||
|
}
|
||||||
|
clientImplCloser := storage.NewFileOpts(storage.NewFileClientOpts{
|
||||||
|
ClientBaseDir: cfg.DataDir,
|
||||||
|
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
|
||||||
|
if dir, ok := f.torrentPaths[infoHash.String()]; ok {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
return baseDir
|
||||||
|
},
|
||||||
|
PieceCompletion: pieceCompletion,
|
||||||
|
})
|
||||||
|
cfg.DefaultStorage = clientImplCloser
|
||||||
|
client, err = torrent.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f.torrentPaths = make(map[string]string)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
|
if err := f.addTorrent(req.URL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.torrent.Drop()
|
||||||
|
res := &base.Resource{
|
||||||
|
Name: f.torrent.Name(),
|
||||||
|
Range: true,
|
||||||
|
RootDir: f.torrent.Name(),
|
||||||
|
Files: make([]*base.FileInfo, len(f.torrent.Files())),
|
||||||
|
Hash: 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()
|
||||||
|
}
|
||||||
|
f.meta.Req = req
|
||||||
|
f.meta.Res = res
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
|
f.meta.Opts = opts
|
||||||
|
if len(opts.SelectFiles) == 0 {
|
||||||
|
opts.SelectFiles = make([]int, 0)
|
||||||
|
for i := range f.torrent.Files() {
|
||||||
|
opts.SelectFiles = append(opts.SelectFiles, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.Path != "" {
|
||||||
|
f.torrentPaths[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
|
f.ready.Store(false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Start() (err error) {
|
||||||
|
if !f.ready.Load() {
|
||||||
|
if err = f.addTorrent(f.meta.Req.URL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files := f.torrent.Files()
|
||||||
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||||
|
f.torrent.DownloadAll()
|
||||||
|
} else {
|
||||||
|
for _, selectIndex := range f.meta.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) Wait() (err error) {
|
||||||
|
for {
|
||||||
|
if f.ready.Load() {
|
||||||
|
done := true
|
||||||
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
|
file := f.torrent.Files()[selectIndex]
|
||||||
|
if file.BytesCompleted() < file.Length() {
|
||||||
|
done = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
// remove unselected files
|
||||||
|
for i, file := range f.torrent.Files() {
|
||||||
|
selected := false
|
||||||
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
|
if i == selectIndex {
|
||||||
|
selected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !selected {
|
||||||
|
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 500)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||||
|
return f.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
|
if !f.ready.Load() {
|
||||||
|
return f.progress
|
||||||
|
}
|
||||||
|
for i := range f.progress {
|
||||||
|
selectIndex := f.meta.Opts.SelectFiles[i]
|
||||||
|
file := f.torrent.Files()[selectIndex]
|
||||||
|
f.progress[i] = file.BytesCompleted()
|
||||||
|
}
|
||||||
|
return f.progress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) addTorrent(url string) (err error) {
|
||||||
|
schema := util.ParseSchema(url)
|
||||||
|
if schema == "MAGNET" {
|
||||||
|
f.torrent, err = client.AddMagnet(url)
|
||||||
|
} else {
|
||||||
|
f.torrent, err = client.AddTorrentFromFile(url)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var cfg config
|
||||||
|
exist, err := f.ctl.GetConfig(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exist && len(cfg.Trackers) > 0 {
|
||||||
|
announceList := make([][]string, 0)
|
||||||
|
for _, tracker := range cfg.Trackers {
|
||||||
|
announceList = append(announceList, []string{tracker})
|
||||||
|
}
|
||||||
|
f.torrent.AddTrackers(announceList)
|
||||||
|
}
|
||||||
|
<-f.torrent.GotInfo()
|
||||||
|
f.ready.Store(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetcherBuilder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var schemes = []string{"FILE", "MAGNET"}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Schemes() []string {
|
||||||
|
return schemes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
||||||
|
return &Fetcher{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
|
return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
|
return &Fetcher{
|
||||||
|
meta: meta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||||
|
doResolve(t, buildFetcher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Config(t *testing.T) {
|
||||||
|
doResolve(t, buildConfigFetcher())
|
||||||
|
}
|
||||||
|
|
||||||
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &base.Resource{
|
||||||
|
Name: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
Size: 1466714112,
|
||||||
|
Range: true,
|
||||||
|
RootDir: "ubuntu-22.04-live-server-amd64.iso",
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFetcher() fetcher.Fetcher {
|
||||||
|
fetcher := new(FetcherBuilder).Build()
|
||||||
|
fetcher.Setup(controller.NewController())
|
||||||
|
return fetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildConfigFetcher() 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, error) {
|
||||||
|
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
|
return fetcher
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Connections int `json:"connections"`
|
||||||
|
}
|
||||||
+195
-100
@@ -4,19 +4,19 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/monkeyWie/gopeed-core/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,51 +33,67 @@ func (re *RequestError) Error() string {
|
|||||||
return fmt.Sprintf("http request fail,code:%d", re.Code)
|
return fmt.Sprintf("http request fail,code:%d", re.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 Fetcher struct {
|
type Fetcher struct {
|
||||||
*fetcher.DefaultFetcher
|
ctl *controller.Controller
|
||||||
|
doneCh chan error
|
||||||
|
|
||||||
res *base.Resource
|
meta *fetcher.FetcherMeta
|
||||||
opts *base.Options
|
|
||||||
status base.Status
|
status base.Status
|
||||||
clients []*http.Response
|
chunks []*chunk
|
||||||
chunks []*Chunk
|
|
||||||
|
|
||||||
|
file *os.File
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
pauseCh chan interface{}
|
pauseCh chan interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFetcher() *Fetcher {
|
func (f *Fetcher) Name() string {
|
||||||
return &Fetcher{
|
return "http"
|
||||||
DefaultFetcher: new(fetcher.DefaultFetcher),
|
|
||||||
pauseCh: make(chan interface{}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var protocols = []string{"HTTP", "HTTPS"}
|
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
||||||
|
f.ctl = ctl
|
||||||
func FetcherBuilder() ([]string, func() fetcher.Fetcher) {
|
f.doneCh = make(chan error, 1)
|
||||||
return protocols, func() fetcher.Fetcher {
|
f.pauseCh = make(chan interface{}, 1)
|
||||||
return NewFetcher()
|
if f.meta == nil {
|
||||||
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
|
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
httpReq, err := buildRequest(nil, req)
|
httpReq, err := buildRequest(nil, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
client := buildClient()
|
client := buildClient()
|
||||||
// 只访问一个字节,测试资源是否支持Range请求
|
// 只访问一个字节,测试资源是否支持Range请求
|
||||||
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
|
||||||
httpResp, err := client.Do(httpReq)
|
httpResp, err := client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
// 拿到响应头就关闭,不用加defer
|
// 拿到响应头就关闭,不用加defer
|
||||||
httpResp.Body.Close()
|
httpResp.Body.Close()
|
||||||
res := &base.Resource{
|
res := &base.Resource{
|
||||||
Req: req,
|
|
||||||
Range: false,
|
Range: false,
|
||||||
Files: []*base.FileInfo{},
|
Files: []*base.FileInfo{},
|
||||||
}
|
}
|
||||||
@@ -89,9 +105,9 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
|||||||
if contentTotal != "" {
|
if contentTotal != "" {
|
||||||
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
res.TotalSize = parse
|
res.Size = parse
|
||||||
}
|
}
|
||||||
} else if base.HttpCodeOK == httpResp.StatusCode {
|
} else if base.HttpCodeOK == httpResp.StatusCode {
|
||||||
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
|
||||||
@@ -99,100 +115,100 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
|
|||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
parse, err := strconv.ParseInt(contentLength, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
res.TotalSize = parse
|
res.Size = parse
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, NewRequestError(httpResp.StatusCode, httpResp.Status)
|
return NewRequestError(httpResp.StatusCode, httpResp.Status)
|
||||||
}
|
}
|
||||||
file := &base.FileInfo{
|
file := &base.FileInfo{
|
||||||
Size: res.TotalSize,
|
Size: res.Size,
|
||||||
}
|
}
|
||||||
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
contentDisposition := httpResp.Header.Get(base.HttpHeaderContentDisposition)
|
||||||
if contentDisposition != "" {
|
if contentDisposition != "" {
|
||||||
_, params, _ := mime.ParseMediaType(contentDisposition)
|
_, params, _ := mime.ParseMediaType(contentDisposition)
|
||||||
filename := params["filename"]
|
filename := params["filePath"]
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
file.Name = filename
|
file.Name = filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get file filename by URL
|
// Get file filePath by URL
|
||||||
if file.Name == "" && strings.Count(req.URL, "/") > 2 {
|
|
||||||
file.Name = filepath.Base(req.URL)
|
|
||||||
}
|
|
||||||
// unknown file filename
|
|
||||||
if file.Name == "" {
|
if file.Name == "" {
|
||||||
|
file.Name = path.Base(httpReq.URL.Path)
|
||||||
|
}
|
||||||
|
// unknown file filePath
|
||||||
|
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
||||||
file.Name = "unknown"
|
file.Name = "unknown"
|
||||||
}
|
}
|
||||||
res.Files = append(res.Files, file)
|
res.Files = append(res.Files, file)
|
||||||
return res, nil
|
res.Name = file.Name
|
||||||
|
f.meta.Req = req
|
||||||
|
f.meta.Res = res
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
|
func (f *Fetcher) Create(opts *base.Options) error {
|
||||||
if opts.Connections != 1 && !res.Range {
|
f.meta.Opts = opts
|
||||||
opts.Connections = 1
|
|
||||||
}
|
|
||||||
f.res = res
|
|
||||||
f.opts = opts
|
|
||||||
f.status = base.DownloadStatusReady
|
f.status = base.DownloadStatusReady
|
||||||
|
|
||||||
|
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opts.Extra == nil {
|
||||||
|
opts.Extra = &fhttp.OptsExtra{}
|
||||||
|
}
|
||||||
|
extra := opts.Extra.(*fhttp.OptsExtra)
|
||||||
|
if extra.Connections == 0 {
|
||||||
|
var cfg config
|
||||||
|
exist, err := f.ctl.GetConfig(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exist {
|
||||||
|
extra.Connections = cfg.Connections
|
||||||
|
} else {
|
||||||
|
extra.Connections = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Start() (err error) {
|
func (f *Fetcher) Start() (err error) {
|
||||||
// 创建文件
|
// 创建文件
|
||||||
name := f.filename()
|
name := f.filepath()
|
||||||
_, err = f.Ctl.Touch(name, f.res.TotalSize)
|
f.file, err = f.ctl.Touch(name, f.meta.Res.Size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.status = base.DownloadStatusStart
|
f.status = base.DownloadStatusRunning
|
||||||
if f.res.Range {
|
f.chunks = f.splitChunk()
|
||||||
// 每个连接平均需要下载的分块大小
|
|
||||||
chunkSize := f.res.TotalSize / int64(f.opts.Connections)
|
|
||||||
f.chunks = make([]*Chunk, f.opts.Connections)
|
|
||||||
f.clients = make([]*http.Response, f.opts.Connections)
|
|
||||||
for i := 0; i < f.opts.Connections; i++ {
|
|
||||||
var (
|
|
||||||
begin = chunkSize * int64(i)
|
|
||||||
end int64
|
|
||||||
)
|
|
||||||
if i == f.opts.Connections-1 {
|
|
||||||
// 最后一个分块需要保证把文件下载完
|
|
||||||
end = f.res.TotalSize - 1
|
|
||||||
} else {
|
|
||||||
end = begin + chunkSize - 1
|
|
||||||
}
|
|
||||||
chunk := NewChunk(begin, end)
|
|
||||||
f.chunks[i] = chunk
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 只支持单连接下载
|
|
||||||
f.chunks = make([]*Chunk, 1)
|
|
||||||
f.clients = make([]*http.Response, 1)
|
|
||||||
f.chunks[0] = NewChunk(0, 0)
|
|
||||||
}
|
|
||||||
f.fetch()
|
f.fetch()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Pause() (err error) {
|
func (f *Fetcher) Pause() (err error) {
|
||||||
if base.DownloadStatusStart != f.status {
|
if base.DownloadStatusRunning != f.status {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.status = base.DownloadStatusPause
|
f.status = base.DownloadStatusPause
|
||||||
|
|
||||||
|
if f.cancel != nil {
|
||||||
f.cancel()
|
f.cancel()
|
||||||
<-f.pauseCh
|
<-f.pauseCh
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Continue() (err error) {
|
func (f *Fetcher) Continue() (err error) {
|
||||||
if base.DownloadStatusStart == f.status || base.DownloadStatusDone == f.status {
|
if base.DownloadStatusRunning == f.status || base.DownloadStatusDone == f.status {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.status = base.DownloadStatusStart
|
f.status = base.DownloadStatusRunning
|
||||||
var name = f.filename()
|
f.file, err = os.OpenFile(f.filepath(), os.O_RDWR, os.ModeAppend)
|
||||||
_, err = f.Ctl.Open(name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -200,6 +216,17 @@ func (f *Fetcher) Continue() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Close() (err error) {
|
||||||
|
if err = f.Pause(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
||||||
|
return f.meta
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
p := make(fetcher.Progress, 0)
|
p := make(fetcher.Progress, 0)
|
||||||
if len(f.chunks) > 0 {
|
if len(f.chunks) > 0 {
|
||||||
@@ -212,19 +239,18 @@ func (f *Fetcher) Progress() fetcher.Progress {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) filename() string {
|
func (f *Fetcher) Wait() (err error) {
|
||||||
// 创建文件
|
return <-f.doneCh
|
||||||
var filename = f.opts.Name
|
|
||||||
if filename == "" {
|
|
||||||
filename = f.res.Files[0].Name
|
|
||||||
}
|
}
|
||||||
return filepath.Join(f.opts.Path, filename)
|
|
||||||
|
func (f *Fetcher) filepath() string {
|
||||||
|
return f.meta.Filepath(f.meta.Res.Files[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) fetch() {
|
func (f *Fetcher) fetch() {
|
||||||
f.ctx, f.cancel = context.WithCancel(context.Background())
|
f.ctx, f.cancel = context.WithCancel(context.Background())
|
||||||
eg, _ := errgroup.WithContext(f.ctx)
|
eg, _ := errgroup.WithContext(f.ctx)
|
||||||
for i := 0; i < f.opts.Connections; i++ {
|
for i := 0; i < len(f.chunks); i++ {
|
||||||
i := i
|
i := i
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return f.fetchChunk(i)
|
return f.fetchChunk(i)
|
||||||
@@ -234,7 +260,7 @@ func (f *Fetcher) fetch() {
|
|||||||
go func() {
|
go func() {
|
||||||
err := eg.Wait()
|
err := eg.Wait()
|
||||||
// 下载停止,关闭文件句柄
|
// 下载停止,关闭文件句柄
|
||||||
f.Ctl.Close(f.filename())
|
f.file.Close()
|
||||||
if f.status == base.DownloadStatusPause {
|
if f.status == base.DownloadStatusPause {
|
||||||
f.pauseCh <- nil
|
f.pauseCh <- nil
|
||||||
} else {
|
} else {
|
||||||
@@ -243,16 +269,15 @@ func (f *Fetcher) fetch() {
|
|||||||
} else {
|
} else {
|
||||||
f.status = base.DownloadStatusDone
|
f.status = base.DownloadStatusDone
|
||||||
}
|
}
|
||||||
f.DoneCh <- err
|
f.doneCh <- err
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) fetchChunk(index int) (err error) {
|
func (f *Fetcher) fetchChunk(index int) (err error) {
|
||||||
filename := f.filename()
|
|
||||||
chunk := f.chunks[index]
|
chunk := f.chunks[index]
|
||||||
|
|
||||||
httpReq, err := buildRequest(f.ctx, f.res.Req)
|
httpReq, err := buildRequest(f.ctx, f.meta.Req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -274,7 +299,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
|||||||
resp *http.Response
|
resp *http.Response
|
||||||
retry bool
|
retry bool
|
||||||
)
|
)
|
||||||
if f.res.Range {
|
if f.meta.Res.Range {
|
||||||
httpReq.Header.Set(base.HttpHeaderRange,
|
httpReq.Header.Set(base.HttpHeaderRange,
|
||||||
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
|
||||||
} else {
|
} else {
|
||||||
@@ -285,7 +310,6 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.clients[index] = resp
|
|
||||||
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
|
||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
return err
|
return err
|
||||||
@@ -303,7 +327,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
|||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := resp.Body.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
_, err := f.Ctl.Write(filename, chunk.Begin+chunk.Downloaded, buf[:n])
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -324,16 +348,45 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.status == base.DownloadStatusPause {
|
|
||||||
chunk.Status = base.DownloadStatusPause
|
|
||||||
} else if chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
|
||||||
chunk.Status = base.DownloadStatusDone
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
chunk.Status = base.DownloadStatusError
|
chunk.Status = base.DownloadStatusError
|
||||||
} else {
|
return
|
||||||
chunk.Status = base.DownloadStatusDone
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
|
||||||
|
if f.status == base.DownloadStatusPause && !isComplete {
|
||||||
|
chunk.Status = base.DownloadStatusPause
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.Status = base.DownloadStatusDone
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
||||||
|
if f.meta.Res.Range {
|
||||||
|
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
|
||||||
|
// 每个连接平均需要下载的分块大小
|
||||||
|
chunkSize := f.meta.Res.Size / int64(connections)
|
||||||
|
chunks = make([]*chunk, connections)
|
||||||
|
for i := 0; i < connections; i++ {
|
||||||
|
var (
|
||||||
|
begin = chunkSize * int64(i)
|
||||||
|
end int64
|
||||||
|
)
|
||||||
|
if i == connections-1 {
|
||||||
|
// 最后一个分块需要保证把文件下载完
|
||||||
|
end = f.meta.Res.Size - 1
|
||||||
|
} else {
|
||||||
|
end = begin + chunkSize - 1
|
||||||
|
}
|
||||||
|
chunk := newChunk(begin, end)
|
||||||
|
chunks[i] = chunk
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 只支持单连接下载
|
||||||
|
chunks = make([]*chunk, 1)
|
||||||
|
chunks[0] = newChunk(0, 0)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -343,7 +396,7 @@ func buildClient() *http.Client {
|
|||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Jar: jar,
|
Jar: jar,
|
||||||
Timeout: time.Second * 10,
|
Timeout: time.Second * 60,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +414,7 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
|
|||||||
if req.Extra == nil {
|
if req.Extra == nil {
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
} else {
|
} else {
|
||||||
extra := req.Extra.(Extra)
|
extra := req.Extra.(*fhttp.ReqExtra)
|
||||||
if extra.Method != "" {
|
if extra.Method != "" {
|
||||||
method = extra.Method
|
method = extra.Method
|
||||||
} else {
|
} else {
|
||||||
@@ -373,7 +426,7 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if extra.Body != "" {
|
if extra.Body != "" {
|
||||||
body = ioutil.NopCloser(bytes.NewBufferString(extra.Body))
|
body = bytes.NewBufferString(extra.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,3 +441,45 @@ func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request
|
|||||||
httpReq.Header = headers
|
httpReq.Header = headers
|
||||||
return httpReq, nil
|
return httpReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fetcherData struct {
|
||||||
|
Chunks []*chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetcherBuilder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var schemes = []string{"HTTP", "HTTPS"}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Schemes() []string {
|
||||||
|
return schemes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Build() fetcher.Fetcher {
|
||||||
|
return &Fetcher{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
|
||||||
|
_f := f.(*Fetcher)
|
||||||
|
return &fetcherData{
|
||||||
|
Chunks: _f.chunks,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
|
||||||
|
return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
|
||||||
|
fd := v.(*fetcherData)
|
||||||
|
fb := &FetcherBuilder{}
|
||||||
|
fetcher := fb.Build().(*Fetcher)
|
||||||
|
fetcher.meta = meta
|
||||||
|
fetcher.status = base.DownloadStatusPause
|
||||||
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
|
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||||
|
if len(fd.Chunks) == 0 {
|
||||||
|
fetcher.chunks = fetcher.splitChunk()
|
||||||
|
} else {
|
||||||
|
fetcher.chunks = fd.Chunks
|
||||||
|
}
|
||||||
|
return fetcher
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/monkeyWie/gopeed-core/internal/controller"
|
"encoding/json"
|
||||||
"github.com/monkeyWie/gopeed-core/internal/fetcher"
|
"fmt"
|
||||||
"github.com/monkeyWie/gopeed-core/pkg/base"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/monkeyWie/gopeed-core/pkg/test"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -13,7 +16,8 @@ import (
|
|||||||
|
|
||||||
func TestFetcher_Resolve(t *testing.T) {
|
func TestFetcher_Resolve(t *testing.T) {
|
||||||
testResolve(test.StartTestFileServer, &base.Resource{
|
testResolve(test.StartTestFileServer, &base.Resource{
|
||||||
TotalSize: test.BuildSize,
|
Name: test.BuildName,
|
||||||
|
Size: test.BuildSize,
|
||||||
Range: true,
|
Range: true,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
{
|
{
|
||||||
@@ -23,7 +27,8 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
testResolve(test.StartTestChunkedServer, &base.Resource{
|
testResolve(test.StartTestChunkedServer, &base.Resource{
|
||||||
TotalSize: 0,
|
Name: test.BuildName,
|
||||||
|
Size: 0,
|
||||||
Range: false,
|
Range: false,
|
||||||
Files: []*base.FileInfo{
|
Files: []*base.FileInfo{
|
||||||
{
|
{
|
||||||
@@ -37,16 +42,15 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
func testResolve(startTestServer func() net.Listener, want *base.Resource, t *testing.T) {
|
func testResolve(startTestServer func() net.Listener, want *base.Resource, t *testing.T) {
|
||||||
listener := startTestServer()
|
listener := startTestServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
fetcher := NewFetcher()
|
fetcher := buildFetcher()
|
||||||
res, err := fetcher.Resolve(&base.Request{
|
err := fetcher.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)
|
||||||
}
|
}
|
||||||
res.Req = nil
|
if !reflect.DeepEqual(want, fetcher.meta.Res) {
|
||||||
if !reflect.DeepEqual(want, res) {
|
t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
|
||||||
t.Errorf("Resolve error = %v, want %v", res, want)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +79,14 @@ func TestFetcher_DownloadChunked(t *testing.T) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
// chunked编码下载
|
// chunked编码下载
|
||||||
downloadNormal(listener, 1, t)
|
downloadNormal(listener, 1, t)
|
||||||
downloadContinue(listener, 1, t)
|
downloadNormal(listener, 2, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadPost(t *testing.T) {
|
||||||
|
listener := test.StartTestPostServer()
|
||||||
|
defer listener.Close()
|
||||||
|
// post下载
|
||||||
|
downloadPost(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadRetry(t *testing.T) {
|
func TestFetcher_DownloadRetry(t *testing.T) {
|
||||||
@@ -92,25 +103,60 @@ func TestFetcher_DownloadError(t *testing.T) {
|
|||||||
downloadError(listener, 1, t)
|
downloadError(listener, 1, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadResume(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadResume(listener, 1, t)
|
||||||
|
downloadResume(listener, 5, t)
|
||||||
|
downloadResume(listener, 8, t)
|
||||||
|
downloadResume(listener, 16, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Config(t *testing.T) {
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 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 {
|
func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
|
||||||
fetcher := NewFetcher()
|
return doDownloadReady(buildFetcher(), listener, connections, t)
|
||||||
fetcher.Setup(controller.NewController())
|
}
|
||||||
res, err := fetcher.Resolve(&base.Request{
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
err = fetcher.Create(res, &base.Options{
|
var extra any = nil
|
||||||
|
if connections > 0 {
|
||||||
|
extra = http.OptsExtra{
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts := &base.Options{
|
||||||
Name: test.DownloadName,
|
Name: test.DownloadName,
|
||||||
Path: test.Dir,
|
Path: test.Dir,
|
||||||
Connections: connections,
|
Extra: extra,
|
||||||
})
|
}
|
||||||
|
err = f.Create(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return fetcher
|
return f
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
||||||
@@ -126,7 +172,31 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
|
|||||||
want := test.FileMd5(test.BuildFile)
|
want := test.FileMd5(test.BuildFile)
|
||||||
got := test.FileMd5(test.DownloadFile)
|
got := test.FileMd5(test.DownloadFile)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("Download error = %v, want %v", got, want)
|
t.Errorf("Download() got = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadPost(listener net.Listener, connections int, t *testing.T) {
|
||||||
|
fetcher := downloadReady(listener, connections, t)
|
||||||
|
fetcher.Meta().Req.Extra = &http.ReqExtra{
|
||||||
|
Method: "POST",
|
||||||
|
Header: map[string]string{
|
||||||
|
"Authorization": "Bearer 123456",
|
||||||
|
},
|
||||||
|
Body: fmt.Sprintf(`{"name":"%s"}`, test.BuildName),
|
||||||
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,11 +206,11 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 200)
|
time.Sleep(time.Millisecond * 50)
|
||||||
if err := fetcher.Pause(); err != nil {
|
if err := fetcher.Pause(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 200)
|
time.Sleep(time.Millisecond * 50)
|
||||||
if err := fetcher.Continue(); err != nil {
|
if err := fetcher.Continue(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -151,7 +221,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
|
|||||||
want := test.FileMd5(test.BuildFile)
|
want := test.FileMd5(test.BuildFile)
|
||||||
got := test.FileMd5(test.DownloadFile)
|
got := test.FileMd5(test.DownloadFile)
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf("Download error = %v, want %v", got, want)
|
t.Errorf("Download() got = %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +233,63 @@ func downloadError(listener net.Listener, connections int, t *testing.T) {
|
|||||||
}
|
}
|
||||||
err = fetcher.Wait()
|
err = fetcher.Wait()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Download error = %v, want %v", err, nil)
|
t.Errorf("Download() got = %v, want %v", err, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadResume(listener net.Listener, connections int, t *testing.T) {
|
||||||
|
fetcher := downloadReady(listener, connections, t)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fb := new(FetcherBuilder)
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
data, err := fb.Store(fetcher)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
fetcher.Pause()
|
||||||
|
|
||||||
|
_, f := fb.Restore()
|
||||||
|
f(fetcher.Meta(), data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fetcher.Setup(controller.NewController())
|
||||||
|
fetcher.Continue()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
fetcher := new(FetcherBuilder).Build()
|
||||||
|
fetcher.Setup(controller.NewController())
|
||||||
|
return fetcher.(*Fetcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildConfigFetcher() fetcher.Fetcher {
|
||||||
|
fetcher := new(FetcherBuilder).Build()
|
||||||
|
newController := controller.NewController()
|
||||||
|
mockCfg := config{
|
||||||
|
Connections: 16,
|
||||||
|
}
|
||||||
|
newController.GetConfig = func(v any) (bool, error) {
|
||||||
|
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
fetcher.Setup(newController)
|
||||||
|
return fetcher
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import "github.com/monkeyWie/gopeed-core/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
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,6 +27,25 @@ func StartTestFileServer() net.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SlowFileServer struct {
|
||||||
|
delay time.Duration
|
||||||
|
handler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
time.Sleep(s.delay)
|
||||||
|
s.handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||||
|
return startTestServer(func() http.Handler {
|
||||||
|
return &SlowFileServer{
|
||||||
|
delay: delay,
|
||||||
|
handler: http.FileServer(http.Dir(Dir)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func StartTestChunkedServer() net.Listener {
|
func StartTestChunkedServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -61,6 +82,29 @@ func StartTestRetryServer() net.Listener {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StartTestPostServer() net.Listener {
|
||||||
|
return startTestServer(func() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if data["name"] == BuildName {
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return mux
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func StartTestErrorServer() net.Listener {
|
func StartTestErrorServer() net.Listener {
|
||||||
counter := 0
|
counter := 0
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func() http.Handler {
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FileMd5(filePath string) string {
|
func FileMd5(filePath string) string {
|
||||||
@@ -26,7 +27,29 @@ func FileMd5(filePath string) string {
|
|||||||
return hex.EncodeToString(hash.Sum(nil))
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DirMd5(dirPath string) string {
|
||||||
|
hash := md5.New()
|
||||||
|
filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
func ToJson(v interface{}) string {
|
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)
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
request := &http.Request{
|
|
||||||
Method: "get",
|
|
||||||
URL: "http://github.com/proxyee-down-org/proxyee-down/releases/download/3.4/proxyee-down-main.jar",
|
|
||||||
Header: map[string]string{
|
|
||||||
"Host": "github.com",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
|
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
|
||||||
"Referer": "http://github.com/proxyee-down-org/proxyee-down/releases",
|
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
got, _ := http.Resolve(request)
|
|
||||||
fmt.Println(got)
|
|
||||||
}
|
|
||||||
+7
-12
@@ -1,15 +1,14 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import "errors"
|
type Status string
|
||||||
|
|
||||||
type Status int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DownloadStatusReady Status = iota
|
DownloadStatusReady Status = "ready" // task create but not start
|
||||||
DownloadStatusStart
|
DownloadStatusRunning Status = "running"
|
||||||
DownloadStatusPause
|
DownloadStatusPause Status = "pause"
|
||||||
DownloadStatusError
|
DownloadStatusWait Status = "wait" // task is wait for running
|
||||||
DownloadStatusDone
|
DownloadStatusError Status = "error"
|
||||||
|
DownloadStatusDone Status = "done"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,7 +22,3 @@ const (
|
|||||||
|
|
||||||
HttpHeaderRangeFormat = "bytes=%d-%d"
|
HttpHeaderRangeFormat = "bytes=%d-%d"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
DeleteErr = errors.New("delete")
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package base
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
NotFound = errors.New("not found")
|
||||||
|
BadParams = errors.New("bad params")
|
||||||
|
)
|
||||||
+58
-23
@@ -1,36 +1,71 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
// 下载请求
|
import (
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request download request
|
||||||
type Request struct {
|
type Request struct {
|
||||||
// 下载链接
|
URL string `json:"url"`
|
||||||
URL string
|
Extra any `json:"extra"`
|
||||||
// 附加信息
|
|
||||||
Extra interface{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 资源信息
|
// Resource download resource
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
Req *Request
|
Name string `json:"name"`
|
||||||
// 资源总大小
|
Size int64 `json:"size"`
|
||||||
TotalSize int64
|
// is support range download
|
||||||
// 是否支持断点下载
|
Range bool `json:"range"`
|
||||||
Range bool
|
RootDir string `json:"rootDir"`
|
||||||
// 资源所包含的文件列表
|
// file list
|
||||||
Files []*FileInfo
|
Files []*FileInfo `json:"files"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
Path string
|
Path string `json:"path"`
|
||||||
Size int64
|
Size int64 `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载选项
|
// Options for download
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// 保存文件名
|
// Download file name
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
// 保存目录
|
// Download file path
|
||||||
Path string
|
Path string `json:"path"`
|
||||||
// 并发连接数
|
// Select file indexes to download
|
||||||
Connections int
|
SelectFiles []int `json:"selectFiles"`
|
||||||
|
// Extra info for specific fetcher
|
||||||
|
Extra any `json:"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
|
||||||
}
|
}
|
||||||
+619
-125
File diff suppressed because it is too large.
Load diff
Loaded 50 of 311 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user